diff --git a/LegendaryExplorer/LegendaryExplorer/AppResources.xaml b/LegendaryExplorer/LegendaryExplorer/AppResources.xaml
index f62bd55a2e..b8fe1a5339 100644
--- a/LegendaryExplorer/LegendaryExplorer/AppResources.xaml
+++ b/LegendaryExplorer/LegendaryExplorer/AppResources.xaml
@@ -1,28 +1,234 @@
-
- /LegendaryExplorer;component/Resources/Fonts/TitilliumWeb-Italic.ttf#Titillium Web
- /LegendaryExplorer;component/Resources/Fonts/TitilliumWeb-Light.ttf#Titillium Web
- /LegendaryExplorer;component/Resources/Fonts/TitilliumWeb-SemiBold.ttf#Titillium Web
- /LegendaryExplorer;component/Resources/Fonts/TitilliumWeb-Regular.ttf#Titillium Web
- /LegendaryExplorer;component/Resources/Fonts/Hack-Bold.ttf#Hack
- /LegendaryExplorer;component/Resources/Fonts/Exo-SemiBold.ttf#Exo
- /LegendaryExplorer;component/Resources/Fonts/Exo-Regular.ttf#Exo
- /LegendaryExplorer;component/Resources/Fonts/BIOMASS2-LIGHT.TTF#Biomass2 Light
- /LegendaryExplorer;component/Resources/Fonts/BIOMASS2-BOLD.TTF#Biomass2 Bold
+
-
-
+
+
+
+
+
- 300
-
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/LegendaryExplorer/LegendaryExplorer/Dialogs/AddPropertyDialog.xaml b/LegendaryExplorer/LegendaryExplorer/Dialogs/AddPropertyDialog.xaml
index 63f2da0641..dd91ca7038 100644
--- a/LegendaryExplorer/LegendaryExplorer/Dialogs/AddPropertyDialog.xaml
+++ b/LegendaryExplorer/LegendaryExplorer/Dialogs/AddPropertyDialog.xaml
@@ -1,4 +1,4 @@
-Add New String Range
Enter the range of String IDs to add. Blank strings will be created for each ID in the range.
- ⚠ Only make strings you need to use.
+ ? Only make strings you need to use.
Do not add unused strings, as these may override other mods using those values.
diff --git a/LegendaryExplorer/LegendaryExplorer/Dialogs/ClassPickerDlg.xaml b/LegendaryExplorer/LegendaryExplorer/Dialogs/ClassPickerDlg.xaml
index be63068b1d..c364fdda57 100644
--- a/LegendaryExplorer/LegendaryExplorer/Dialogs/ClassPickerDlg.xaml
+++ b/LegendaryExplorer/LegendaryExplorer/Dialogs/ClassPickerDlg.xaml
@@ -1,4 +1,4 @@
-
-
+
-
+
+
+
+
+
+
diff --git a/LegendaryExplorer/LegendaryExplorer/Dialogs/ListDialog.xaml.cs b/LegendaryExplorer/LegendaryExplorer/Dialogs/ListDialog.xaml.cs
index 03dcaeb96b..5e0589a9b7 100644
--- a/LegendaryExplorer/LegendaryExplorer/Dialogs/ListDialog.xaml.cs
+++ b/LegendaryExplorer/LegendaryExplorer/Dialogs/ListDialog.xaml.cs
@@ -4,6 +4,7 @@
using System.Windows.Input;
using LegendaryExplorer.SharedUI.Bases;
using LegendaryExplorerCore.Misc;
+using MessageBox = Xceed.Wpf.Toolkit.MessageBox;
namespace LegendaryExplorer.Dialogs
{
diff --git a/LegendaryExplorer/LegendaryExplorer/Dialogs/NamePromptDialog.xaml b/LegendaryExplorer/LegendaryExplorer/Dialogs/NamePromptDialog.xaml
index a2e5cbaf4e..f8c0a4cefb 100644
--- a/LegendaryExplorer/LegendaryExplorer/Dialogs/NamePromptDialog.xaml
+++ b/LegendaryExplorer/LegendaryExplorer/Dialogs/NamePromptDialog.xaml
@@ -1,4 +1,4 @@
-
-
\ No newline at end of file
+
diff --git a/LegendaryExplorer/LegendaryExplorer/Dialogs/PromptDialog.xaml b/LegendaryExplorer/LegendaryExplorer/Dialogs/PromptDialog.xaml
index 9c13bf8ea6..ec5fb8b043 100644
--- a/LegendaryExplorer/LegendaryExplorer/Dialogs/PromptDialog.xaml
+++ b/LegendaryExplorer/LegendaryExplorer/Dialogs/PromptDialog.xaml
@@ -1,4 +1,4 @@
-
-
\ No newline at end of file
+
diff --git a/LegendaryExplorer/LegendaryExplorer/Dialogs/SelectOrAddNamePromptDialog.xaml b/LegendaryExplorer/LegendaryExplorer/Dialogs/SelectOrAddNamePromptDialog.xaml
index ba166c0709..20581156b8 100644
--- a/LegendaryExplorer/LegendaryExplorer/Dialogs/SelectOrAddNamePromptDialog.xaml
+++ b/LegendaryExplorer/LegendaryExplorer/Dialogs/SelectOrAddNamePromptDialog.xaml
@@ -1,4 +1,4 @@
-
-
\ No newline at end of file
+
diff --git a/LegendaryExplorer/LegendaryExplorer/Dialogs/SelectOrAddNamePromptDialog.xaml.cs b/LegendaryExplorer/LegendaryExplorer/Dialogs/SelectOrAddNamePromptDialog.xaml.cs
index 6a5284a41f..5b90717817 100644
--- a/LegendaryExplorer/LegendaryExplorer/Dialogs/SelectOrAddNamePromptDialog.xaml.cs
+++ b/LegendaryExplorer/LegendaryExplorer/Dialogs/SelectOrAddNamePromptDialog.xaml.cs
@@ -7,6 +7,7 @@
using LegendaryExplorerCore.Misc;
using LegendaryExplorerCore.Packages;
using LegendaryExplorerCore.Unreal;
+using MessageBox = Xceed.Wpf.Toolkit.MessageBox;
namespace LegendaryExplorer.Dialogs
{
diff --git a/LegendaryExplorer/LegendaryExplorer/Dialogs/SetWwisePathDialog.xaml b/LegendaryExplorer/LegendaryExplorer/Dialogs/SetWwisePathDialog.xaml
index 4ad01590ea..056fdae9cf 100644
--- a/LegendaryExplorer/LegendaryExplorer/Dialogs/SetWwisePathDialog.xaml
+++ b/LegendaryExplorer/LegendaryExplorer/Dialogs/SetWwisePathDialog.xaml
@@ -1,4 +1,4 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/LegendaryExplorer/LegendaryExplorer/Dialogs/ShiftInterpTrackDialog.xaml.cs b/LegendaryExplorer/LegendaryExplorer/Dialogs/ShiftInterpTrackDialog.xaml.cs
new file mode 100644
index 0000000000..853d030541
--- /dev/null
+++ b/LegendaryExplorer/LegendaryExplorer/Dialogs/ShiftInterpTrackDialog.xaml.cs
@@ -0,0 +1,70 @@
+using System.Windows;
+using MessageBox = Xceed.Wpf.Toolkit.MessageBox;
+
+namespace LegendaryExplorer.Dialogs
+{
+ public class ShiftInterpTrackParameters
+ {
+ public float OffsetX { get; set; }
+ public float OffsetY { get; set; }
+ public float OffsetZ { get; set; }
+ public float Roll { get; set; }
+ public float Pitch { get; set; }
+ public float Yaw { get; set; }
+ public float TimeOffset { get; set; }
+
+ public ShiftInterpTrackParameters()
+ {
+ OffsetX = 0;
+ OffsetY = 0;
+ OffsetZ = 0;
+ Roll = 0;
+ Pitch = 0;
+ Yaw = 0;
+ TimeOffset = 0;
+ }
+ }
+
+ public partial class ShiftInterpTrackDialog : Window
+ {
+ public ShiftInterpTrackParameters Parameters { get; private set; }
+
+ public ShiftInterpTrackDialog()
+ {
+ Parameters = new ShiftInterpTrackParameters();
+ InitializeComponent();
+ }
+
+ private void OkButton_Click(object sender, RoutedEventArgs e)
+ {
+ if (!float.TryParse(OffsetXTextBox.Text, out var offsetX) ||
+ !float.TryParse(OffsetYTextBox.Text, out var offsetY) ||
+ !float.TryParse(OffsetZTextBox.Text, out var offsetZ) ||
+ !float.TryParse(RollTextBox.Text, out var roll) ||
+ !float.TryParse(PitchTextBox.Text, out var pitch) ||
+ !float.TryParse(YawTextBox.Text, out var yaw) ||
+ !float.TryParse(TimeOffsetTextBox.Text, out var timeOffset))
+ {
+ MessageBox.Show("All fields must contain valid numbers.", "Invalid Input", MessageBoxButton.OK, MessageBoxImage.Warning);
+ return;
+ }
+
+ Parameters.OffsetX = offsetX;
+ Parameters.OffsetY = offsetY;
+ Parameters.OffsetZ = offsetZ;
+ Parameters.Roll = roll;
+ Parameters.Pitch = pitch;
+ Parameters.Yaw = yaw;
+ Parameters.TimeOffset = timeOffset;
+
+ DialogResult = true;
+ Close();
+ }
+
+ private void CancelButton_Click(object sender, RoutedEventArgs e)
+ {
+ DialogResult = false;
+ Close();
+ }
+ }
+}
diff --git a/LegendaryExplorer/LegendaryExplorer/Dialogs/SoundReplaceOptionsDialog.xaml b/LegendaryExplorer/LegendaryExplorer/Dialogs/SoundReplaceOptionsDialog.xaml
index ee63c6f868..96d115ae7d 100644
--- a/LegendaryExplorer/LegendaryExplorer/Dialogs/SoundReplaceOptionsDialog.xaml
+++ b/LegendaryExplorer/LegendaryExplorer/Dialogs/SoundReplaceOptionsDialog.xaml
@@ -1,4 +1,4 @@
-
+
+
+
+
+
+
+
+
+
+
+ M0,0 L10,0 L10,10 L0,10 Z
+ M2,0 L10,0 L10,8 L8,8 L8,2 L2,2 Z M0,2 L8,2 L8,10 L0,10 Z
+
+
+
+
+
diff --git a/LegendaryExplorer/LegendaryExplorer/LegendaryExplorer/LegendaryExplorer/SharedUI/CustomWindowChrome.cs b/LegendaryExplorer/LegendaryExplorer/LegendaryExplorer/LegendaryExplorer/SharedUI/CustomWindowChrome.cs
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/LegendaryExplorer/LegendaryExplorer/MainWindow/About.xaml.cs b/LegendaryExplorer/LegendaryExplorer/LegendaryExplorer/MainWindow/About.xaml.cs
similarity index 74%
rename from LegendaryExplorer/LegendaryExplorer/MainWindow/About.xaml.cs
rename to LegendaryExplorer/LegendaryExplorer/LegendaryExplorer/MainWindow/About.xaml.cs
index b247881d48..21f1fa941b 100644
--- a/LegendaryExplorer/LegendaryExplorer/MainWindow/About.xaml.cs
+++ b/LegendaryExplorer/LegendaryExplorer/LegendaryExplorer/MainWindow/About.xaml.cs
@@ -1,6 +1,7 @@
-using System;
+using System;
using System.Reflection;
using System.Windows;
+using LegendaryExplorer.SharedUI;
namespace LegendaryExplorer.MainWindow
{
@@ -12,6 +13,7 @@ public partial class About : Window
public About()
{
InitializeComponent();
+ CustomWindowChrome.ApplyCustomChrome(this);
}
}
}
diff --git a/LegendaryExplorer/LegendaryExplorer/MainWindow/SettingsWindow.xaml.cs b/LegendaryExplorer/LegendaryExplorer/LegendaryExplorer/MainWindow/SettingsWindow.xaml.cs
similarity index 98%
rename from LegendaryExplorer/LegendaryExplorer/MainWindow/SettingsWindow.xaml.cs
rename to LegendaryExplorer/LegendaryExplorer/LegendaryExplorer/MainWindow/SettingsWindow.xaml.cs
index d57843787d..5a50a86412 100644
--- a/LegendaryExplorer/LegendaryExplorer/MainWindow/SettingsWindow.xaml.cs
+++ b/LegendaryExplorer/LegendaryExplorer/LegendaryExplorer/MainWindow/SettingsWindow.xaml.cs
@@ -7,6 +7,7 @@
using DocumentFormat.OpenXml.Drawing;
using LegendaryExplorer.Misc;
using LegendaryExplorer.Misc.AppSettings;
+using LegendaryExplorer.SharedUI;
using LegendaryExplorerCore;
using LegendaryExplorerCore.GameFilesystem;
using Microsoft.WindowsAPICodePack.Dialogs;
@@ -22,6 +23,7 @@ public partial class SettingsWindow : Window
public SettingsWindow()
{
InitializeComponent();
+ CustomWindowChrome.ApplyCustomChrome(this);
}
///
@@ -129,4 +131,4 @@ private void AssociateOthers_Click(object sender, RoutedEventArgs e)
FileAssociations.AssociateOthers();
}
}
-}
\ No newline at end of file
+}
diff --git a/LegendaryExplorer/LegendaryExplorer/LegendaryExplorer/Misc/AppSettings/SettingsDefinitions.xml b/LegendaryExplorer/LegendaryExplorer/LegendaryExplorer/Misc/AppSettings/SettingsDefinitions.xml
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/LegendaryExplorer/LegendaryExplorer/LegendaryExplorer/Misc/NotifyPropertyChangedBase.cs b/LegendaryExplorer/LegendaryExplorer/LegendaryExplorer/Misc/NotifyPropertyChangedBase.cs
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/LegendaryExplorer/LegendaryExplorer/LegendaryExplorer/Misc/ThemeManager.cs b/LegendaryExplorer/LegendaryExplorer/LegendaryExplorer/Misc/ThemeManager.cs
new file mode 100644
index 0000000000..2eae1ed9e2
--- /dev/null
+++ b/LegendaryExplorer/LegendaryExplorer/LegendaryExplorer/Misc/ThemeManager.cs
@@ -0,0 +1,234 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using System.Windows;
+using Be.Windows.Forms;
+using LegendaryExplorer.Misc.AppSettings;
+using Color = System.Drawing.Color;
+
+namespace LegendaryExplorer.Misc
+{
+ ///
+ /// Manages application theme switching between light and dark modes.
+ ///
+ public static class ThemeManager
+ {
+ private const string DarkThemeUri = "/LegendaryExplorer;component/DarkTheme.xaml";
+ private static ResourceDictionary _darkThemeDictionary;
+
+ // Track registered HexBox controls for theme updates
+ private static readonly List> _registeredHexBoxes = new();
+
+ ///
+ /// Event that fires when the theme changes. Subscribe to this to update custom themed controls.
+ ///
+ public static event EventHandler ThemeChanged;
+
+ ///
+ /// Applies the current theme based on settings.
+ ///
+ public static void ApplyTheme()
+ {
+ ApplyTheme(Settings.Global_DarkMode_Enabled);
+ }
+
+ ///
+ /// Applies the specified theme.
+ ///
+ /// True for dark mode, false for light mode.
+ public static void ApplyTheme(bool isDarkMode)
+ {
+ if (Application.Current == null)
+ return;
+
+ var mergedDictionaries = Application.Current.Resources.MergedDictionaries;
+
+ if (isDarkMode)
+ {
+ // Load and apply dark theme if not already applied
+ if (_darkThemeDictionary == null)
+ {
+ _darkThemeDictionary = new ResourceDictionary
+ {
+ Source = new Uri(DarkThemeUri, UriKind.Relative)
+ };
+ }
+
+ if (!mergedDictionaries.Contains(_darkThemeDictionary))
+ {
+ mergedDictionaries.Add(_darkThemeDictionary);
+ }
+
+ // Also set the static HexBox colors so new instances get dark colors
+ HexBox.SetColors(Color.FromArgb(0x1E, 0x1E, 0x1E), Color.FromArgb(0xE0, 0xE0, 0xE0));
+ }
+ else
+ {
+ // Remove dark theme to revert to system defaults
+ if (_darkThemeDictionary != null && mergedDictionaries.Contains(_darkThemeDictionary))
+ {
+ mergedDictionaries.Remove(_darkThemeDictionary);
+ }
+
+ // Reset static HexBox colors to light theme
+ HexBox.SetColors(Color.White, Color.Black);
+ }
+
+ // Update all registered HexBox controls
+ UpdateAllHexBoxThemes(isDarkMode);
+
+ // Fire the ThemeChanged event to notify subscribers
+ ThemeChanged?.Invoke(null, isDarkMode);
+ }
+
+ ///
+ /// Registers a HexBox control for theme management and applies current theme.
+ /// Call this when a HexBox is loaded.
+ ///
+ /// The HexBox control to register.
+ public static void RegisterHexBox(HexBox hexBox)
+ {
+ if (hexBox == null) return;
+
+ // Clean up dead references and check if already registered
+ _registeredHexBoxes.RemoveAll(wr => !wr.TryGetTarget(out _));
+
+ // Check if already registered
+ foreach (var weakRef in _registeredHexBoxes)
+ {
+ if (weakRef.TryGetTarget(out var existingHexBox) && existingHexBox == hexBox)
+ return;
+ }
+
+ _registeredHexBoxes.Add(new WeakReference(hexBox));
+
+ // Apply theme immediately
+ ApplyHexBoxTheme(hexBox, Settings.Global_DarkMode_Enabled);
+
+ // Hook into HandleCreated to reapply after control is fully initialized
+ hexBox.HandleCreated += (s, e) => ApplyHexBoxTheme(hexBox, Settings.Global_DarkMode_Enabled);
+
+ // Hook into VisibleChanged for when the control becomes visible
+ hexBox.VisibleChanged += (s, e) =>
+ {
+ if (hexBox.Visible)
+ ApplyHexBoxTheme(hexBox, Settings.Global_DarkMode_Enabled);
+ };
+
+ // Schedule another apply after delays to ensure rendering is complete
+ Task.Delay(50).ContinueWith(_ =>
+ {
+ try
+ {
+ if (!hexBox.IsDisposed)
+ ApplyHexBoxTheme(hexBox, Settings.Global_DarkMode_Enabled);
+ }
+ catch { }
+ });
+
+ Task.Delay(200).ContinueWith(_ =>
+ {
+ try
+ {
+ if (!hexBox.IsDisposed)
+ ApplyHexBoxTheme(hexBox, Settings.Global_DarkMode_Enabled);
+ }
+ catch { }
+ });
+ }
+
+ ///
+ /// Applies the current theme colors to a HexBox control.
+ ///
+ /// The HexBox control to theme.
+ /// Whether dark mode is enabled.
+ public static void ApplyHexBoxTheme(HexBox hexBox, bool isDarkMode)
+ {
+ if (hexBox == null || hexBox.IsDisposed) return;
+
+ // Apply dark mode to the scrollbar
+ hexBox.ScrollBarDarkMode = isDarkMode;
+
+ if (isDarkMode)
+ {
+ // Dark theme colors - set all color properties explicitly
+ hexBox.BackColor = Color.FromArgb(0x1E, 0x1E, 0x1E); // DarkBackground #FF1E1E1E
+ hexBox.ForeColor = Color.FromArgb(0xE0, 0xE0, 0xE0); // DarkText #FFE0E0E0
+ hexBox.InfoForeColor = Color.FromArgb(0xB0, 0xB0, 0xB0); // DarkTextSecondary #FFB0B0B0
+ hexBox.SelectionBackColor = Color.FromArgb(0x00, 0x7A, 0xCC); // DarkHighlight #FF007ACC
+ hexBox.SelectionForeColor = Color.White; // DarkHighlightText
+ hexBox.HighlightBackColor = Color.FromArgb(0x26, 0x4F, 0x78); // DarkSelection #FF264F78
+ hexBox.HighlightForeColor = Color.FromArgb(0xFF, 0xFF, 0xE0); // Light yellow for visibility
+ hexBox.BackColorDisabled = Color.FromArgb(0x2D, 0x2D, 0x30); // DarkControl #FF2D2D30
+ }
+ else
+ {
+ // Light theme colors (defaults)
+ hexBox.BackColor = Color.White;
+ hexBox.ForeColor = Color.Black;
+ hexBox.InfoForeColor = Color.Gray;
+ hexBox.SelectionBackColor = Color.Blue;
+ hexBox.SelectionForeColor = Color.White;
+ hexBox.HighlightBackColor = Color.Yellow;
+ hexBox.HighlightForeColor = Color.Black;
+ hexBox.BackColorDisabled = Color.FromName("WhiteSmoke");
+ }
+
+ // Force immediate synchronous refresh for WinForms control hosted in WPF
+ try
+ {
+ if (hexBox.InvokeRequired)
+ {
+ hexBox.Invoke(() => PerformHexBoxRefresh(hexBox));
+ }
+ else
+ {
+ PerformHexBoxRefresh(hexBox);
+ }
+ }
+ catch
+ {
+ // Control might be disposed
+ }
+ }
+
+ ///
+ /// Performs the actual refresh operations on the HexBox control
+ ///
+ private static void PerformHexBoxRefresh(HexBox hexBox)
+ {
+ try
+ {
+ hexBox.Invalidate(true);
+ hexBox.Update();
+ hexBox.Refresh();
+
+ // Also refresh parent
+ if (hexBox.Parent != null)
+ {
+ hexBox.Parent.Invalidate(true);
+ hexBox.Parent.Update();
+ try { hexBox.Parent.Refresh(); } catch { }
+ }
+ }
+ catch { }
+ }
+
+ ///
+ /// Updates all registered HexBox controls with the current theme.
+ ///
+ private static void UpdateAllHexBoxThemes(bool isDarkMode)
+ {
+ // Clean up dead references as we iterate
+ _registeredHexBoxes.RemoveAll(wr => !wr.TryGetTarget(out _));
+
+ foreach (var weakRef in _registeredHexBoxes)
+ {
+ if (weakRef.TryGetTarget(out var hexBox))
+ {
+ ApplyHexBoxTheme(hexBox, isDarkMode);
+ }
+ }
+ }
+ }
+}
diff --git a/LegendaryExplorer/LegendaryExplorer/LegendaryExplorer/SharedUI/CustomWindowChrome.cs b/LegendaryExplorer/LegendaryExplorer/LegendaryExplorer/SharedUI/CustomWindowChrome.cs
new file mode 100644
index 0000000000..5e6a9d10fe
--- /dev/null
+++ b/LegendaryExplorer/LegendaryExplorer/LegendaryExplorer/SharedUI/CustomWindowChrome.cs
@@ -0,0 +1,246 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.InteropServices;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+using System.Windows.Interop;
+using System.Windows.Shell;
+using LegendaryExplorer.Misc;
+using LegendaryExplorer.Misc.AppSettings;
+
+namespace LegendaryExplorer.SharedUI
+{
+ ///
+ /// Provides attached properties for custom window chrome behavior that integrates with the app's theming system.
+ /// This class enables dark/light mode title bars on Windows 10/11 using the DWM API,
+ /// and automatically updates when the app's theme setting changes.
+ ///
+ public static class CustomWindowChrome
+ {
+ #region Win32 DWM Interop for dark mode title bar
+
+ [DllImport("dwmapi.dll", PreserveSig = true)]
+ private static extern unsafe int DwmSetWindowAttribute(IntPtr hwnd, int attr, int* attrValue, int attrSize);
+
+ // DWMWA_USE_IMMERSIVE_DARK_MODE - Windows 10 20H1+ and Windows 11
+ private const int DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1 = 19;
+ private const int DWMWA_USE_IMMERSIVE_DARK_MODE = 20;
+
+ // DWMWA_CAPTION_COLOR - Windows 11 only
+ private const int DWMWA_CAPTION_COLOR = 35;
+
+ // DWMWA_BORDER_COLOR - Windows 11 only
+ private const int DWMWA_BORDER_COLOR = 34;
+
+ #endregion
+
+ #region Window Tracking
+
+ // Track registered windows for theme updates using weak references
+ private static readonly List> _registeredWindows = new();
+ private static bool _themeChangedSubscribed;
+
+ ///
+ /// Ensures we're subscribed to theme changes.
+ ///
+ private static void EnsureThemeChangeSubscription()
+ {
+ if (!_themeChangedSubscribed)
+ {
+ ThemeManager.ThemeChanged += OnThemeChanged;
+ _themeChangedSubscribed = true;
+ }
+ }
+
+ ///
+ /// Handler for theme changes - updates all registered windows.
+ ///
+ private static void OnThemeChanged(object sender, bool isDarkMode)
+ {
+ // Clean up dead references and update all live windows
+ _registeredWindows.RemoveAll(wr => !wr.TryGetTarget(out _));
+
+ foreach (var weakRef in _registeredWindows)
+ {
+ if (weakRef.TryGetTarget(out var window))
+ {
+ ApplyThemeToWindowHandle(window, isDarkMode);
+ }
+ }
+ }
+
+ ///
+ /// Registers a window for theme management.
+ ///
+ private static void RegisterWindow(Window window)
+ {
+ if (window == null) return;
+
+ // Clean up dead references
+ _registeredWindows.RemoveAll(wr => !wr.TryGetTarget(out _));
+
+ // Check if already registered
+ foreach (var weakRef in _registeredWindows)
+ {
+ if (weakRef.TryGetTarget(out var existingWindow) && existingWindow == window)
+ return;
+ }
+
+ _registeredWindows.Add(new WeakReference(window));
+
+ // Remove from tracking when window closes
+ window.Closed += (s, e) =>
+ {
+ _registeredWindows.RemoveAll(wr =>
+ !wr.TryGetTarget(out var w) || w == window);
+ };
+ }
+
+ #endregion
+
+ #region EnableCustomChrome Attached Property
+
+ public static readonly DependencyProperty EnableCustomChromeProperty =
+ DependencyProperty.RegisterAttached(
+ "EnableCustomChrome",
+ typeof(bool),
+ typeof(CustomWindowChrome),
+ new PropertyMetadata(false, OnEnableCustomChromeChanged));
+
+ public static bool GetEnableCustomChrome(DependencyObject obj) =>
+ (bool)obj.GetValue(EnableCustomChromeProperty);
+
+ public static void SetEnableCustomChrome(DependencyObject obj, bool value) =>
+ obj.SetValue(EnableCustomChromeProperty, value);
+
+ private static void OnEnableCustomChromeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ if (d is Window window && (bool)e.NewValue)
+ {
+ ApplyCustomChrome(window);
+ }
+ }
+
+ #endregion
+
+ #region TitleBarHeight Attached Property
+
+ public static readonly DependencyProperty TitleBarHeightProperty =
+ DependencyProperty.RegisterAttached(
+ "TitleBarHeight",
+ typeof(double),
+ typeof(CustomWindowChrome),
+ new PropertyMetadata(30.0));
+
+ public static double GetTitleBarHeight(DependencyObject obj) =>
+ (double)obj.GetValue(TitleBarHeightProperty);
+
+ public static void SetTitleBarHeight(DependencyObject obj, double value) =>
+ obj.SetValue(TitleBarHeightProperty, value);
+
+ #endregion
+
+ ///
+ /// Applies themed chrome to the window's native title bar using Windows DWM API.
+ /// This keeps the standard Windows minimize/maximize/close buttons but renders
+ /// them in dark or light mode style based on the app's current theme setting.
+ /// The title bar will automatically update when the theme changes.
+ ///
+ public static void ApplyCustomChrome(Window window)
+ {
+ if (window == null) return;
+
+ // Ensure we're subscribed to theme changes
+ EnsureThemeChangeSubscription();
+
+ // Register this window for theme updates
+ RegisterWindow(window);
+
+ // If the window is already loaded, apply immediately
+ if (window.IsLoaded)
+ {
+ ApplyThemeToWindowHandle(window, Settings.Global_DarkMode_Enabled);
+ }
+ else
+ {
+ // Otherwise, wait for the window to load
+ window.Loaded += (s, e) => ApplyThemeToWindowHandle(window, Settings.Global_DarkMode_Enabled);
+ }
+ }
+
+ private static unsafe void ApplyThemeToWindowHandle(Window window, bool isDarkMode)
+ {
+ var hwnd = new WindowInteropHelper(window).Handle;
+ if (hwnd == IntPtr.Zero) return;
+
+ int useDarkMode = isDarkMode ? 1 : 0;
+
+ // Try the Windows 10 20H1+ / Windows 11 attribute first
+ int result = DwmSetWindowAttribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE, &useDarkMode, sizeof(int));
+
+ // If that fails, try the older attribute for earlier Windows 10 builds
+ if (result != 0)
+ {
+ DwmSetWindowAttribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1, &useDarkMode, sizeof(int));
+ }
+ }
+ }
+
+ ///
+ /// Commands for custom window chrome title bar buttons.
+ ///
+ public static class WindowCommands
+ {
+ public static readonly RoutedCommand Minimize = new RoutedCommand("Minimize", typeof(WindowCommands));
+ public static readonly RoutedCommand MaximizeRestore = new RoutedCommand("MaximizeRestore", typeof(WindowCommands));
+ public static readonly RoutedCommand Close = new RoutedCommand("Close", typeof(WindowCommands));
+
+ static WindowCommands()
+ {
+ // Register command bindings at the application level
+ CommandManager.RegisterClassCommandBinding(typeof(Window),
+ new CommandBinding(Minimize, OnMinimizeExecuted, OnCanExecute));
+ CommandManager.RegisterClassCommandBinding(typeof(Window),
+ new CommandBinding(MaximizeRestore, OnMaximizeRestoreExecuted, OnCanExecute));
+ CommandManager.RegisterClassCommandBinding(typeof(Window),
+ new CommandBinding(Close, OnCloseExecuted, OnCanExecute));
+ }
+
+ private static void OnCanExecute(object sender, CanExecuteRoutedEventArgs e)
+ {
+ e.CanExecute = true;
+ }
+
+ private static void OnMinimizeExecuted(object sender, ExecutedRoutedEventArgs e)
+ {
+ if (sender is Window window)
+ {
+ SystemCommands.MinimizeWindow(window);
+ }
+ }
+
+ private static void OnMaximizeRestoreExecuted(object sender, ExecutedRoutedEventArgs e)
+ {
+ if (sender is Window window)
+ {
+ if (window.WindowState == WindowState.Maximized)
+ {
+ SystemCommands.RestoreWindow(window);
+ }
+ else
+ {
+ SystemCommands.MaximizeWindow(window);
+ }
+ }
+ }
+
+ private static void OnCloseExecuted(object sender, ExecutedRoutedEventArgs e)
+ {
+ if (sender is Window window)
+ {
+ SystemCommands.CloseWindow(window);
+ }
+ }
+ }
+}
diff --git a/LegendaryExplorer/LegendaryExplorer/LegendaryExplorer/SharedUI/ToolBarBehavior.cs b/LegendaryExplorer/LegendaryExplorer/LegendaryExplorer/SharedUI/ToolBarBehavior.cs
new file mode 100644
index 0000000000..e7759f5a19
--- /dev/null
+++ b/LegendaryExplorer/LegendaryExplorer/LegendaryExplorer/SharedUI/ToolBarBehavior.cs
@@ -0,0 +1,231 @@
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Controls.Primitives;
+using System.Windows.Data;
+using System.Windows.Media;
+
+namespace LegendaryExplorer.SharedUI
+{
+ ///
+ /// Attached behavior for ToolBar controls that styles the overflow button and its popup to match the toolbar's background.
+ /// This removes the white background from the overflow arrow button and dropdown that appears in toolbars.
+ ///
+ public static class ToolBarBehavior
+ {
+ public static readonly DependencyProperty RemoveOverflowButtonWhiteBackgroundProperty = DependencyProperty.RegisterAttached(
+ "RemoveOverflowButtonWhiteBackground", typeof(bool), typeof(ToolBarBehavior), new PropertyMetadata(false, OnPropertyChanged));
+
+ private static void OnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ if (d is ToolBar toolBar)
+ {
+ var enable = (bool)e.NewValue;
+ if (enable)
+ {
+ toolBar.Loaded += OnToolBarLoaded;
+ // If already loaded, apply immediately
+ if (toolBar.IsLoaded)
+ {
+ ApplyOverflowButtonStyle(toolBar);
+ }
+ }
+ else
+ {
+ toolBar.Loaded -= OnToolBarLoaded;
+ }
+ }
+ }
+
+ private static void OnToolBarLoaded(object sender, RoutedEventArgs e)
+ {
+ if (sender is ToolBar toolBar)
+ {
+ ApplyOverflowButtonStyle(toolBar);
+ }
+ }
+
+ private static void ApplyOverflowButtonStyle(ToolBar toolBar)
+ {
+ // Find the OverflowGrid by its template part name
+ if (toolBar.Template?.FindName("OverflowGrid", toolBar) is Grid overflowGrid)
+ {
+ // Bind the overflow grid's background to match the toolbar
+ overflowGrid.SetBinding(Panel.BackgroundProperty, new Binding(nameof(ToolBar.Background))
+ {
+ Source = toolBar,
+ Mode = BindingMode.OneWay
+ });
+
+ // Find the ToggleButton inside and style its template elements
+ if (FindChild(overflowGrid) is ToggleButton overflowButton)
+ {
+ // Set the button's background to transparent so the grid's background shows through
+ overflowButton.Background = Brushes.Transparent;
+
+ // Find any Border elements inside the button's visual tree and make them transparent
+ StyleOverflowButtonVisuals(overflowButton);
+ }
+ }
+
+ // Style the overflow popup/dropdown
+ StyleOverflowPopup(toolBar);
+ }
+
+ ///
+ /// Styles the overflow popup that appears when the overflow button is clicked.
+ ///
+ private static void StyleOverflowPopup(ToolBar toolBar)
+ {
+ // Find the Popup named "OverflowPopup" in the toolbar template
+ if (toolBar.Template?.FindName("OverflowPopup", toolBar) is Popup overflowPopup)
+ {
+ // When the popup opens, style its contents
+ overflowPopup.Opened += (s, e) =>
+ {
+ if (overflowPopup.Child != null)
+ {
+ StylePopupContents(overflowPopup.Child, toolBar);
+ }
+ };
+
+ // If popup already has a child, style it now
+ if (overflowPopup.Child != null)
+ {
+ StylePopupContents(overflowPopup.Child, toolBar);
+ }
+ }
+
+ // Also look for the ToolBarOverflowPanel directly
+ if (toolBar.Template?.FindName("PART_ToolBarOverflowPanel", toolBar) is ToolBarOverflowPanel overflowPanel)
+ {
+ overflowPanel.SetBinding(Panel.BackgroundProperty, new Binding(nameof(ToolBar.Background))
+ {
+ Source = toolBar,
+ Mode = BindingMode.OneWay
+ });
+ }
+ }
+
+ ///
+ /// Styles the contents of the overflow popup to match the toolbar background.
+ ///
+ private static void StylePopupContents(UIElement popupChild, ToolBar toolBar)
+ {
+ // Style any Border elements in the popup
+ if (popupChild is Border border)
+ {
+ border.SetBinding(Border.BackgroundProperty, new Binding(nameof(ToolBar.Background))
+ {
+ Source = toolBar,
+ Mode = BindingMode.OneWay
+ });
+
+ // Also style children of the border
+ if (border.Child != null)
+ {
+ StylePopupContents(border.Child, toolBar);
+ }
+ }
+ else if (popupChild is Panel panel)
+ {
+ panel.SetBinding(Panel.BackgroundProperty, new Binding(nameof(ToolBar.Background))
+ {
+ Source = toolBar,
+ Mode = BindingMode.OneWay
+ });
+
+ // Style children of the panel
+ foreach (UIElement child in panel.Children)
+ {
+ StylePopupContents(child, toolBar);
+ }
+ }
+ else if (popupChild is FrameworkElement element)
+ {
+ // Walk the visual tree for other elements
+ int childCount = VisualTreeHelper.GetChildrenCount(element);
+ for (int i = 0; i < childCount; i++)
+ {
+ if (VisualTreeHelper.GetChild(element, i) is UIElement child)
+ {
+ StylePopupContents(child, toolBar);
+ }
+ }
+ }
+ }
+
+ ///
+ /// Makes the overflow button's internal borders transparent so the parent background shows through.
+ ///
+ private static void StyleOverflowButtonVisuals(ToggleButton button)
+ {
+ // We need to wait for the button's template to be applied
+ button.ApplyTemplate();
+
+ // Walk the visual tree and make backgrounds transparent
+ int childCount = VisualTreeHelper.GetChildrenCount(button);
+ for (int i = 0; i < childCount; i++)
+ {
+ var child = VisualTreeHelper.GetChild(button, i);
+ SetTransparentBackgrounds(child);
+ }
+ }
+
+ ///
+ /// Recursively sets backgrounds to transparent for Border and Panel elements.
+ ///
+ private static void SetTransparentBackgrounds(DependencyObject element)
+ {
+ if (element is Border border)
+ {
+ border.Background = Brushes.Transparent;
+ }
+ else if (element is Panel panel && panel is not ToolBarPanel && panel is not ToolBarOverflowPanel)
+ {
+ panel.Background = Brushes.Transparent;
+ }
+
+ int childCount = VisualTreeHelper.GetChildrenCount(element);
+ for (int i = 0; i < childCount; i++)
+ {
+ SetTransparentBackgrounds(VisualTreeHelper.GetChild(element, i));
+ }
+ }
+
+ ///
+ /// Finds a child element of the specified type in the visual tree.
+ ///
+ private static T FindChild(DependencyObject parent) where T : DependencyObject
+ {
+ if (parent == null) return null;
+
+ int childCount = VisualTreeHelper.GetChildrenCount(parent);
+ for (int i = 0; i < childCount; i++)
+ {
+ var child = VisualTreeHelper.GetChild(parent, i);
+ if (child is T typedChild)
+ {
+ return typedChild;
+ }
+
+ var result = FindChild(child);
+ if (result != null)
+ {
+ return result;
+ }
+ }
+
+ return null;
+ }
+
+ public static void SetRemoveOverflowButtonWhiteBackground(DependencyObject element, bool value)
+ {
+ element.SetValue(RemoveOverflowButtonWhiteBackgroundProperty, value);
+ }
+
+ public static bool GetRemoveOverflowButtonWhiteBackground(DependencyObject element)
+ {
+ return (bool)element.GetValue(RemoveOverflowButtonWhiteBackgroundProperty);
+ }
+ }
+}
diff --git a/LegendaryExplorer/LegendaryExplorer/LegendaryExplorer/Tools/FaceFXEditor/AutoFaceFXGenerator/AudioAnalyzer.cs b/LegendaryExplorer/LegendaryExplorer/LegendaryExplorer/Tools/FaceFXEditor/AutoFaceFXGenerator/AudioAnalyzer.cs
new file mode 100644
index 0000000000..fd757746f7
--- /dev/null
+++ b/LegendaryExplorer/LegendaryExplorer/LegendaryExplorer/Tools/FaceFXEditor/AutoFaceFXGenerator/AudioAnalyzer.cs
@@ -0,0 +1,253 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using NAudio.Wave;
+using LegendaryExplorerCore.Packages;
+using LegendaryExplorerCore.Unreal.BinaryConverters;
+using LegendaryExplorer.UnrealExtensions.Classes;
+
+namespace LegendaryExplorer.Tools.FaceFXEditor.AutoFaceFXGenerator
+{
+ ///
+ /// Analyzes audio to extract timing and amplitude information for lip sync generation
+ ///
+ public static class AudioAnalyzer
+ {
+ ///
+ /// Gets the duration of audio from a WwiseStream or SoundNodeWave export
+ ///
+ /// The audio export entry
+ /// Duration in seconds, or 0 if unable to determine
+ public static float GetAudioDuration(ExportEntry audioExport)
+ {
+ if (audioExport == null)
+ return 0f;
+
+ try
+ {
+ // Try to get the raw audio data and convert to PCM
+ byte[] audioData = GetAudioAsWav(audioExport);
+ if (audioData == null || audioData.Length == 0)
+ return 0f;
+
+ using var ms = new MemoryStream(audioData);
+ using var reader = new WaveFileReader(ms);
+ return (float)reader.TotalTime.TotalSeconds;
+ }
+ catch
+ {
+ return 0f;
+ }
+ }
+
+ ///
+ /// Analyzes audio amplitude over time to determine emphasis points
+ ///
+ /// The audio export entry
+ /// List of amplitude data points
+ public static List AnalyzeAmplitude(ExportEntry audioExport)
+ {
+ var result = new List();
+
+ if (audioExport == null)
+ return result;
+
+ try
+ {
+ byte[] audioData = GetAudioAsWav(audioExport);
+ if (audioData == null || audioData.Length == 0)
+ return result;
+
+ using var ms = new MemoryStream(audioData);
+ using var reader = new WaveFileReader(ms);
+
+ // Sample every ~20ms for amplitude analysis
+ float sampleInterval = 0.02f;
+ float totalDuration = (float)reader.TotalTime.TotalSeconds;
+ int samplesPerWindow = (int)(reader.WaveFormat.SampleRate * sampleInterval);
+
+ float[] buffer = new float[samplesPerWindow];
+ var sampleProvider = reader.ToSampleProvider();
+
+ float currentTime = 0f;
+ while (currentTime < totalDuration)
+ {
+ int samplesRead = sampleProvider.Read(buffer, 0, samplesPerWindow);
+ if (samplesRead == 0)
+ break;
+
+ // Calculate RMS amplitude for this window
+ float sum = 0f;
+ for (int i = 0; i < samplesRead; i++)
+ {
+ sum += buffer[i] * buffer[i];
+ }
+ float rms = (float)Math.Sqrt(sum / samplesRead);
+
+ result.Add(new AmplitudeData
+ {
+ Time = currentTime,
+ Amplitude = rms
+ });
+
+ currentTime += sampleInterval;
+ }
+
+ // Normalize amplitudes
+ if (result.Count > 0)
+ {
+ float maxAmplitude = result.Max(a => a.Amplitude);
+ if (maxAmplitude > 0)
+ {
+ foreach (var data in result)
+ {
+ data.NormalizedAmplitude = data.Amplitude / maxAmplitude;
+ }
+ }
+ }
+ }
+ catch
+ {
+ // Return empty list on error
+ }
+
+ return result;
+ }
+
+ ///
+ /// Detects voice activity (when someone is speaking vs silence)
+ ///
+ /// Amplitude data from AnalyzeAmplitude
+ /// Amplitude threshold for voice detection (0-1)
+ /// List of voice activity segments
+ public static List DetectVoiceActivity(List amplitudeData, float threshold = 0.1f)
+ {
+ var segments = new List();
+
+ if (amplitudeData.Count == 0)
+ return segments;
+
+ bool inVoice = false;
+ float segmentStart = 0f;
+
+ foreach (var data in amplitudeData)
+ {
+ if (data.NormalizedAmplitude > threshold && !inVoice)
+ {
+ inVoice = true;
+ segmentStart = data.Time;
+ }
+ else if (data.NormalizedAmplitude <= threshold && inVoice)
+ {
+ inVoice = false;
+ segments.Add(new VoiceSegment
+ {
+ StartTime = segmentStart,
+ EndTime = data.Time
+ });
+ }
+ }
+
+ // Handle case where voice continues to the end
+ if (inVoice && amplitudeData.Count > 0)
+ {
+ segments.Add(new VoiceSegment
+ {
+ StartTime = segmentStart,
+ EndTime = amplitudeData.Last().Time
+ });
+ }
+
+ return segments;
+ }
+
+ ///
+ /// Gets audio data as WAV bytes
+ ///
+ private static byte[] GetAudioAsWav(ExportEntry audioExport)
+ {
+ try
+ {
+ if (audioExport.ClassName == "WwiseStream")
+ {
+ // For WwiseStream, get the audio data and convert to WAV
+ return WwiseStreamToWav(audioExport);
+ }
+ else if (audioExport.ClassName == "SoundNodeWave")
+ {
+ // For SoundNodeWave (ME1/LE1), extract and convert
+ return SoundNodeWaveToWav(audioExport);
+ }
+ }
+ catch
+ {
+ // Return null on any error
+ }
+ return null;
+ }
+
+ private static byte[] WwiseStreamToWav(ExportEntry wwiseStreamExport)
+ {
+ try
+ {
+ var stream = wwiseStreamExport.GetBinaryData();
+ if (stream != null)
+ {
+ // Use the extension method to create a wave stream
+ var waveStream = stream.CreateWaveStream();
+ if (waveStream != null)
+ {
+ return waveStream.ToArray();
+ }
+ }
+ }
+ catch
+ {
+ // Return null on conversion error
+ }
+ return null;
+ }
+
+ private static byte[] SoundNodeWaveToWav(ExportEntry soundNodeWave)
+ {
+ try
+ {
+ // Get raw audio data from ISB or internal storage
+ var props = soundNodeWave.GetProperties();
+ var rawData = props.GetProp("RawData");
+ if (rawData != null && rawData.Bytes.Length > 0)
+ {
+ // The raw data should already be PCM or can be converted
+ // This is a simplified implementation
+ return rawData.Bytes;
+ }
+ }
+ catch
+ {
+ // Return null on error
+ }
+ return null;
+ }
+ }
+
+ ///
+ /// Represents amplitude data at a point in time
+ ///
+ public class AmplitudeData
+ {
+ public float Time { get; set; }
+ public float Amplitude { get; set; }
+ public float NormalizedAmplitude { get; set; }
+ }
+
+ ///
+ /// Represents a segment where voice is detected
+ ///
+ public class VoiceSegment
+ {
+ public float StartTime { get; set; }
+ public float EndTime { get; set; }
+ public float Duration => EndTime - StartTime;
+ }
+}
diff --git a/LegendaryExplorer/LegendaryExplorer/LegendaryExplorer/Tools/FaceFXEditor/AutoFaceFXGenerator/AutoFaceFXGenerationDialog.xaml b/LegendaryExplorer/LegendaryExplorer/LegendaryExplorer/Tools/FaceFXEditor/AutoFaceFXGenerator/AutoFaceFXGenerationDialog.xaml
new file mode 100644
index 0000000000..3269905d08
--- /dev/null
+++ b/LegendaryExplorer/LegendaryExplorer/LegendaryExplorer/Tools/FaceFXEditor/AutoFaceFXGenerator/AutoFaceFXGenerationDialog.xaml
@@ -0,0 +1,151 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/LegendaryExplorer/LegendaryExplorer/LegendaryExplorer/Tools/FaceFXEditor/AutoFaceFXGenerator/AutoFaceFXGenerationDialog.xaml.cs b/LegendaryExplorer/LegendaryExplorer/LegendaryExplorer/Tools/FaceFXEditor/AutoFaceFXGenerator/AutoFaceFXGenerationDialog.xaml.cs
new file mode 100644
index 0000000000..16ad179b04
--- /dev/null
+++ b/LegendaryExplorer/LegendaryExplorer/LegendaryExplorer/Tools/FaceFXEditor/AutoFaceFXGenerator/AutoFaceFXGenerationDialog.xaml.cs
@@ -0,0 +1,493 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+using System.Windows;
+using System.Windows.Media;
+using LegendaryExplorerCore.Misc;
+using LegendaryExplorerCore.Packages;
+using Microsoft.Win32;
+using static LegendaryExplorer.UserControls.ExportLoaderControls.FaceFXAnimSetEditorControl;
+using MessageBox = Xceed.Wpf.Toolkit.MessageBox;
+
+namespace LegendaryExplorer.Tools.FaceFXEditor.AutoFaceFXGenerator
+{
+ ///
+ /// Dialog for configuring auto FaceFX generation
+ ///
+ public partial class AutoFaceFXGenerationDialog : Window, INotifyPropertyChanged
+ {
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ // Input data
+ private readonly IFaceFXBinary _faceFX;
+ private readonly LegendaryExplorerCore.Unreal.BinaryConverters.FaceFXLine _line;
+ private readonly ExportEntry _audioExport;
+
+ // Properties for binding
+ public string LineName => _line?.NameAsString ?? "Unknown";
+ public int TLKID { get; }
+
+ private string _tlkText;
+ public string TLKText
+ {
+ get => _tlkText;
+ set { _tlkText = value; OnPropertyChanged(); }
+ }
+
+ private string _audioDurationText;
+ public string AudioDurationText
+ {
+ get => _audioDurationText;
+ set { _audioDurationText = value; OnPropertyChanged(); }
+ }
+
+ // FXA file support (animation curves)
+ private string _fxaFilePath;
+ public string FxaFilePath
+ {
+ get => _fxaFilePath;
+ set
+ {
+ _fxaFilePath = value;
+ OnPropertyChanged();
+ ValidateFxaFile();
+ }
+ }
+
+ private string _fxaStatusText = "No FXA file loaded";
+ public string FxaStatusText
+ {
+ get => _fxaStatusText;
+ set { _fxaStatusText = value; OnPropertyChanged(); }
+ }
+
+ private Brush _fxaStatusColor = Brushes.Gray;
+ public Brush FxaStatusColor
+ {
+ get => _fxaStatusColor;
+ set { _fxaStatusColor = value; OnPropertyChanged(); }
+ }
+
+ // FXT file support (phoneme timing)
+ private string _fxtFilePath;
+ public string FxtFilePath
+ {
+ get => _fxtFilePath;
+ set
+ {
+ _fxtFilePath = value;
+ OnPropertyChanged();
+ ValidateFxtFile();
+ }
+ }
+
+ private string _fxtStatusText = "No FXT file loaded";
+ public string FxtStatusText
+ {
+ get => _fxtStatusText;
+ set { _fxtStatusText = value; OnPropertyChanged(); }
+ }
+
+ private Brush _fxtStatusColor = Brushes.Gray;
+ public Brush FxtStatusColor
+ {
+ get => _fxtStatusColor;
+ set { _fxtStatusColor = value; OnPropertyChanged(); }
+ }
+
+ private bool _useTextAnalysis = true;
+ public bool UseTextAnalysis
+ {
+ get => _useTextAnalysis;
+ set { _useTextAnalysis = value; OnPropertyChanged(); }
+ }
+
+ // Parsed data
+ private FxaAnimationData _fxaData;
+ private FxaAnimationData _fxtData;
+
+ // Generation options
+ private bool _generateBlinkAnimation = true;
+ public bool GenerateBlinkAnimation
+ {
+ get => _generateBlinkAnimation;
+ set { _generateBlinkAnimation = value; OnPropertyChanged(); }
+ }
+
+ private bool _generateEyebrowAnimation = true;
+ public bool GenerateEyebrowAnimation
+ {
+ get => _generateEyebrowAnimation;
+ set { _generateEyebrowAnimation = value; OnPropertyChanged(); }
+ }
+
+ private bool _generateHeadMovement = false;
+ public bool GenerateHeadMovement
+ {
+ get => _generateHeadMovement;
+ set { _generateHeadMovement = value; OnPropertyChanged(); }
+ }
+
+ private float _lipSyncIntensity = 1.0f;
+ public float LipSyncIntensity
+ {
+ get => _lipSyncIntensity;
+ set { _lipSyncIntensity = value; OnPropertyChanged(); }
+ }
+
+ private float _blinkFrequency = 0.2f;
+ public float BlinkFrequency
+ {
+ get => _blinkFrequency;
+ set { _blinkFrequency = value; OnPropertyChanged(); }
+ }
+
+ // Base emotion categories - the code will apply appropriate FaceFX animations based on selected emotion
+ public List AvailableEmotions { get; } = new List
+ {
+ "None",
+ "Anger",
+ "Disgust",
+ "Fear",
+ "Happy",
+ "Sad",
+ "Surprise",
+ "Contempt",
+ "Determined",
+ "Worried"
+ };
+
+ private string _selectedEmotion = "None";
+ public string SelectedEmotion
+ {
+ get => _selectedEmotion;
+ set { _selectedEmotion = value; OnPropertyChanged(); }
+ }
+
+ private float _emotionIntensity = 0.5f;
+ public float EmotionIntensity
+ {
+ get => _emotionIntensity;
+ set { _emotionIntensity = value; OnPropertyChanged(); }
+ }
+
+ // Species selection
+ public List AvailableSpecies { get; } = new List
+ {
+ "Human Female",
+ "Human Male",
+ "Human Child",
+ "Asari",
+ "Krogan",
+ "Drell",
+ "Turian",
+ "Salarian",
+ "Quarian",
+ "Geth",
+ "Elcor",
+ "Hanar",
+ "Volus",
+ "Batarian",
+ "Vorcha",
+ "Prothean",
+ "Yahg"
+ };
+
+ private string _selectedSpecies = "Human Female";
+ public string SelectedSpecies
+ {
+ get => _selectedSpecies;
+ set { _selectedSpecies = value; OnPropertyChanged(); }
+ }
+
+ // Result
+ public bool WasGenerated { get; private set; }
+
+ public AutoFaceFXGenerationDialog(
+ IFaceFXBinary faceFX,
+ LegendaryExplorerCore.Unreal.BinaryConverters.FaceFXLine line,
+ int tlkId,
+ string tlkText,
+ ExportEntry audioExport,
+ Window owner = null)
+ {
+ _faceFX = faceFX;
+ _line = line;
+ _audioExport = audioExport;
+ TLKID = tlkId;
+ TLKText = tlkText ?? "";
+
+ InitializeComponent();
+ DataContext = this;
+
+ if (owner != null)
+ {
+ Owner = owner;
+ }
+
+ // Get audio duration
+ float duration = AudioAnalyzer.GetAudioDuration(audioExport);
+ if (duration > 0)
+ {
+ AudioDurationText = $"{duration:F2} seconds";
+ }
+ else
+ {
+ AudioDurationText = "Unable to determine (will estimate from text)";
+ }
+ }
+
+ private void BrowseFxaButton_Click(object sender, RoutedEventArgs e)
+ {
+ var dialog = new OpenFileDialog
+ {
+ Title = "Select FXA File (Animation Curves)",
+ Filter = "FXA Files (*.fxa)|*.fxa|XML Files (*.xml)|*.xml|All Files (*.*)|*.*",
+ CheckFileExists = true
+ };
+
+ if (dialog.ShowDialog() == true)
+ {
+ FxaFilePath = dialog.FileName;
+ }
+ }
+
+ private void BrowseFxtButton_Click(object sender, RoutedEventArgs e)
+ {
+ var dialog = new OpenFileDialog
+ {
+ Title = "Select FXT File (Phoneme Timing)",
+ Filter = "FXT Files (*.fxt)|*.fxt|Text Files (*.txt)|*.txt|All Files (*.*)|*.*",
+ CheckFileExists = true
+ };
+
+ if (dialog.ShowDialog() == true)
+ {
+ FxtFilePath = dialog.FileName;
+ }
+ }
+
+ private void ValidateFxaFile()
+ {
+ if (string.IsNullOrEmpty(_fxaFilePath))
+ {
+ FxaStatusText = "No FXA file loaded";
+ FxaStatusColor = Brushes.Gray;
+ _fxaData = null;
+ return;
+ }
+
+ try
+ {
+ _fxaData = FxaXmlParser.ParseFxaFile(_fxaFilePath);
+
+ if (_fxaData != null && _fxaData.Animations.Count > 0)
+ {
+ FxaStatusText = $"✓ Loaded {_fxaData.Animations.Count} animation curves";
+ FxaStatusColor = Brushes.Green;
+ }
+ else
+ {
+ FxaStatusText = "⚠ File loaded but no animation curves found";
+ FxaStatusColor = Brushes.Orange;
+ _fxaData = null;
+ }
+ }
+ catch (Exception ex)
+ {
+ FxaStatusText = $"✗ {ex.Message}";
+ FxaStatusColor = Brushes.Red;
+ _fxaData = null;
+ }
+ }
+
+ private void ValidateFxtFile()
+ {
+ if (string.IsNullOrEmpty(_fxtFilePath))
+ {
+ FxtStatusText = "No FXT file loaded";
+ FxtStatusColor = Brushes.Gray;
+ _fxtData = null;
+ return;
+ }
+
+ try
+ {
+ _fxtData = FxaXmlParser.ParseFxtFile(_fxtFilePath);
+
+ if (_fxtData != null && _fxtData.PhonemeEvents.Count > 0)
+ {
+ FxtStatusText = $"✓ Loaded {_fxtData.PhonemeEvents.Count} phoneme events";
+ FxtStatusColor = Brushes.Green;
+ }
+ else if (_fxtData != null && _fxtData.Animations.Count > 0)
+ {
+ FxtStatusText = $"✓ Converted to {_fxtData.Animations.Count} animation curves";
+ FxtStatusColor = Brushes.Green;
+ }
+ else
+ {
+ FxtStatusText = "⚠ File loaded but no phoneme data found";
+ FxtStatusColor = Brushes.Orange;
+ _fxtData = null;
+ }
+ }
+ catch (Exception ex)
+ {
+ FxtStatusText = $"✗ {ex.Message}";
+ FxtStatusColor = Brushes.Red;
+ _fxtData = null;
+ }
+ }
+
+ private void GenerateButton_Click(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ // Parse the selected emotion
+ EmotionType emotion = EmotionType.None;
+ if (!string.IsNullOrEmpty(SelectedEmotion) && SelectedEmotion != "None")
+ {
+ Enum.TryParse(SelectedEmotion, out emotion);
+ }
+
+ // Parse the selected species
+ FaceFXSpecies species = SelectedSpecies switch
+ {
+ "Human Male" => FaceFXSpecies.HumanMale,
+ "Human Child" => FaceFXSpecies.HumanChild,
+ "Asari" => FaceFXSpecies.Asari,
+ "Krogan" => FaceFXSpecies.Krogan,
+ "Drell" => FaceFXSpecies.Drell,
+ "Turian" => FaceFXSpecies.Turian,
+ "Salarian" => FaceFXSpecies.Salarian,
+ "Quarian" => FaceFXSpecies.Quarian,
+ "Geth" => FaceFXSpecies.Geth,
+ "Elcor" => FaceFXSpecies.Elcor,
+ "Hanar" => FaceFXSpecies.Hanar,
+ "Volus" => FaceFXSpecies.Volus,
+ "Batarian" => FaceFXSpecies.Batarian,
+ "Vorcha" => FaceFXSpecies.Vorcha,
+ "Prothean" => FaceFXSpecies.Prothean,
+ "Yahg" => FaceFXSpecies.Yahg,
+ _ => FaceFXSpecies.HumanFemale
+ };
+
+ // FXA/FXT support is disabled for now - just use text analysis
+ var options = new FaceFXGenerationOptions
+ {
+ CharacterType = CharacterType.HumanFemale,
+ Species = species,
+ GenerateJawAnimation = true,
+ GenerateBlinkAnimation = GenerateBlinkAnimation,
+ GenerateEyebrowAnimation = GenerateEyebrowAnimation,
+ GenerateHeadMovement = GenerateHeadMovement,
+ LipSyncIntensity = LipSyncIntensity,
+ BlinkFrequency = BlinkFrequency,
+ UseAudioAmplitude = true,
+ Emotion = emotion,
+ EmotionIntensity = EmotionIntensity,
+ FxaData = null, // Disabled for now
+ UseTextFallback = true
+ };
+
+ var generator = new FaceFXGenerator(_faceFX, _line, TLKText, _audioExport, options);
+ bool success = generator.Generate();
+
+ if (success)
+ {
+ WasGenerated = true;
+ DialogResult = true;
+ Close();
+ }
+ else
+ {
+ string errorMessage = "Failed to generate FaceFX animations.";
+ if (!string.IsNullOrEmpty(generator.LastError))
+ {
+ errorMessage += $"\n\nError: {generator.LastError}";
+ }
+ else
+ {
+ errorMessage += "\n\nPlease provide dialogue text for lip sync generation.";
+ }
+ MessageBox.Show(errorMessage, "Generation Failed", MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+ }
+ catch (Exception ex)
+ {
+ MessageBox.Show($"An error occurred during generation:\n\n{ex.Message}\n\n{ex.StackTrace}",
+ "Error", MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+ }
+
+ ///
+ /// Combine FXA animation curves with FXT phoneme timing data
+ ///
+ private FxaAnimationData CombineFxaAndFxtData()
+ {
+ // If neither file is loaded, return null
+ if (_fxaData == null && _fxtData == null)
+ return null;
+
+ // If only one is loaded, return that one
+ if (_fxaData == null)
+ return _fxtData;
+ if (_fxtData == null)
+ return _fxaData;
+
+ // Combine both datasets
+ var combined = new FxaAnimationData();
+
+ // Start with FXA animations (these are the primary curves)
+ foreach (var kvp in _fxaData.Animations)
+ {
+ combined.Animations[kvp.Key] = kvp.Value;
+ }
+
+ // Merge in FXT-generated animations
+ // If FXT has animations that FXA doesn't have, add them
+ // If both have the same animation, prefer FXA but blend with FXT
+ foreach (var kvp in _fxtData.Animations)
+ {
+ if (!combined.Animations.ContainsKey(kvp.Key))
+ {
+ // FXT has this animation but FXA doesn't - add it
+ combined.Animations[kvp.Key] = kvp.Value;
+ }
+ // If FXA already has this animation, we keep FXA's version
+ // (FXA is considered more authoritative)
+ }
+
+ // Copy phoneme events for reference
+ combined.PhonemeEvents.AddRange(_fxtData.PhonemeEvents);
+
+ // Copy phoneme mapping
+ foreach (var kvp in _fxaData.PhonemeMapping)
+ {
+ combined.PhonemeMapping[kvp.Key] = kvp.Value;
+ }
+ foreach (var kvp in _fxtData.PhonemeMapping)
+ {
+ if (!combined.PhonemeMapping.ContainsKey(kvp.Key))
+ {
+ combined.PhonemeMapping[kvp.Key] = kvp.Value;
+ }
+ }
+
+ return combined;
+ }
+
+ private void CancelButton_Click(object sender, RoutedEventArgs e)
+ {
+ DialogResult = false;
+ Close();
+ }
+
+ protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+ }
+}
diff --git a/LegendaryExplorer/LegendaryExplorer/LegendaryExplorer/Tools/FaceFXEditor/AutoFaceFXGenerator/BulkFaceFXGenerationDialog.xaml b/LegendaryExplorer/LegendaryExplorer/LegendaryExplorer/Tools/FaceFXEditor/AutoFaceFXGenerator/BulkFaceFXGenerationDialog.xaml
new file mode 100644
index 0000000000..55953394a7
--- /dev/null
+++ b/LegendaryExplorer/LegendaryExplorer/LegendaryExplorer/Tools/FaceFXEditor/AutoFaceFXGenerator/BulkFaceFXGenerationDialog.xaml
@@ -0,0 +1,74 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/LegendaryExplorer/LegendaryExplorer/LegendaryExplorer/Tools/FaceFXEditor/AutoFaceFXGenerator/BulkFaceFXGenerationDialog.xaml.cs b/LegendaryExplorer/LegendaryExplorer/LegendaryExplorer/Tools/FaceFXEditor/AutoFaceFXGenerator/BulkFaceFXGenerationDialog.xaml.cs
new file mode 100644
index 0000000000..2ed714262d
--- /dev/null
+++ b/LegendaryExplorer/LegendaryExplorer/LegendaryExplorer/Tools/FaceFXEditor/AutoFaceFXGenerator/BulkFaceFXGenerationDialog.xaml.cs
@@ -0,0 +1,114 @@
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+using System.Windows;
+
+namespace LegendaryExplorer.Tools.FaceFXEditor.AutoFaceFXGenerator
+{
+ ///
+ /// Dialog for configuring bulk FaceFX generation options
+ ///
+ public partial class BulkFaceFXGenerationDialog : Window, INotifyPropertyChanged
+ {
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+
+ public string LineCountText { get; }
+
+ // Species selection
+ public List AvailableSpecies { get; } = new List
+ {
+ "Human Female",
+ "Human Male",
+ "Human Child",
+ "Asari",
+ "Krogan",
+ "Drell",
+ "Turian",
+ "Salarian",
+ "Quarian",
+ "Geth",
+ "Elcor",
+ "Hanar",
+ "Volus",
+ "Batarian",
+ "Vorcha",
+ "Prothean",
+ "Yahg"
+ };
+
+ private string _selectedSpecies = "Human Female";
+ public string SelectedSpecies
+ {
+ get => _selectedSpecies;
+ set { _selectedSpecies = value; OnPropertyChanged(); }
+ }
+
+ private float _lipSyncIntensity = 1.0f;
+ public float LipSyncIntensity
+ {
+ get => _lipSyncIntensity;
+ set { _lipSyncIntensity = value; OnPropertyChanged(); }
+ }
+
+ ///
+ /// The selected species as enum value
+ ///
+ public FaceFXSpecies SelectedSpeciesEnum => SelectedSpecies switch
+ {
+ "Human Male" => FaceFXSpecies.HumanMale,
+ "Human Child" => FaceFXSpecies.HumanChild,
+ "Asari" => FaceFXSpecies.Asari,
+ "Krogan" => FaceFXSpecies.Krogan,
+ "Drell" => FaceFXSpecies.Drell,
+ "Turian" => FaceFXSpecies.Turian,
+ "Salarian" => FaceFXSpecies.Salarian,
+ "Quarian" => FaceFXSpecies.Quarian,
+ "Geth" => FaceFXSpecies.Geth,
+ "Elcor" => FaceFXSpecies.Elcor,
+ "Hanar" => FaceFXSpecies.Hanar,
+ "Volus" => FaceFXSpecies.Volus,
+ "Batarian" => FaceFXSpecies.Batarian,
+ "Vorcha" => FaceFXSpecies.Vorcha,
+ "Prothean" => FaceFXSpecies.Prothean,
+ "Yahg" => FaceFXSpecies.Yahg,
+ _ => FaceFXSpecies.HumanFemale
+ };
+
+ ///
+ /// Whether the user confirmed generation
+ ///
+ public bool Confirmed { get; private set; }
+
+ public BulkFaceFXGenerationDialog(int lineCount, Window owner = null)
+ {
+ LineCountText = $"Generate FaceFX for {lineCount} lines";
+
+ InitializeComponent();
+ DataContext = this;
+
+ if (owner != null)
+ {
+ Owner = owner;
+ }
+ }
+
+ private void GenerateButton_Click(object sender, RoutedEventArgs e)
+ {
+ Confirmed = true;
+ DialogResult = true;
+ Close();
+ }
+
+ private void CancelButton_Click(object sender, RoutedEventArgs e)
+ {
+ Confirmed = false;
+ DialogResult = false;
+ Close();
+ }
+ }
+}
diff --git a/LegendaryExplorer/LegendaryExplorer/LegendaryExplorer/Tools/FaceFXEditor/AutoFaceFXGenerator/FaceFXGenerator.cs b/LegendaryExplorer/LegendaryExplorer/LegendaryExplorer/Tools/FaceFXEditor/AutoFaceFXGenerator/FaceFXGenerator.cs
new file mode 100644
index 0000000000..599bfa5c06
--- /dev/null
+++ b/LegendaryExplorer/LegendaryExplorer/LegendaryExplorer/Tools/FaceFXEditor/AutoFaceFXGenerator/FaceFXGenerator.cs
@@ -0,0 +1,1933 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using LegendaryExplorerCore.Packages;
+using LegendaryExplorerCore.Unreal.BinaryConverters;
+using static LegendaryExplorer.UserControls.ExportLoaderControls.FaceFXAnimSetEditorControl;
+
+namespace LegendaryExplorer.Tools.FaceFXEditor.AutoFaceFXGenerator
+{
+ ///
+ /// Supported character types for FaceFX generation (legacy - use FaceFXSpecies instead)
+ ///
+ public enum CharacterType
+ {
+ HumanFemale,
+ HumanMale,
+ // Future character types can be added here
+ }
+
+ ///
+ /// Available emotion types for FaceFX generation
+ ///
+ public enum EmotionType
+ {
+ None,
+ Anger,
+ Disgust,
+ Fear,
+ Happy,
+ Sad,
+ Surprise,
+ Contempt,
+ Determined,
+ Worried
+ }
+
+ ///
+ /// Options for FaceFX generation
+ ///
+ public class FaceFXGenerationOptions
+ {
+ public CharacterType CharacterType { get; set; } = CharacterType.HumanFemale;
+
+ ///
+ /// Species for FaceFX generation - determines which phoneme-to-viseme mappings to use
+ ///
+ public FaceFXSpecies Species { get; set; } = FaceFXSpecies.HumanFemale;
+
+ public bool GenerateJawAnimation { get; set; } = true;
+ public bool GenerateBlinkAnimation { get; set; } = true;
+ public bool GenerateEyebrowAnimation { get; set; } = true;
+ public bool GenerateHeadMovement { get; set; } = false;
+ public float LipSyncIntensity { get; set; } = 1.0f;
+ public float BlinkFrequency { get; set; } = 0.2f; // Blinks per second
+ public bool UseAudioAmplitude { get; set; } = true;
+
+ ///
+ /// Emotion to apply to the animation
+ ///
+ public EmotionType Emotion { get; set; } = EmotionType.None;
+
+ ///
+ /// Intensity of the emotion (0-1) - higher values create more visible expressions
+ ///
+ public float EmotionIntensity { get; set; } = 0.8f;
+
+ ///
+ /// FXA animation data imported from UDK FaceFX Studio
+ ///
+ public FxaAnimationData FxaData { get; set; }
+
+ ///
+ /// Whether to use text-to-phoneme fallback if no FXA data
+ ///
+ public bool UseTextFallback { get; set; } = true;
+ }
+
+ ///
+ /// Generates FaceFX animations automatically from FXA files or text
+ ///
+ public class FaceFXGenerator
+ {
+ private readonly IFaceFXBinary _faceFX;
+ private readonly FaceFXLine _line;
+ private readonly string _tlkText;
+ private readonly ExportEntry _audioExport;
+ private readonly FaceFXGenerationOptions _options;
+ private float _audioDuration;
+ private List _amplitudeData;
+ private List _phonemes; // Store phoneme data for use by jaw animations
+
+ ///
+ /// Contains the last error message if generation failed
+ ///
+ public string LastError { get; private set; }
+
+ public FaceFXGenerator(IFaceFXBinary faceFX, FaceFXLine line, string tlkText, ExportEntry audioExport, FaceFXGenerationOptions options = null)
+ {
+ _faceFX = faceFX;
+ _line = line;
+ _tlkText = tlkText ?? "";
+ _audioExport = audioExport;
+ _options = options ?? new FaceFXGenerationOptions();
+ }
+
+ ///
+ /// Generates FaceFX animations for the line
+ ///
+ /// True if generation was successful
+ public bool Generate()
+ {
+ try
+ {
+ // Validate inputs
+ if (_faceFX == null || _line == null)
+ return false;
+
+ // Analyze audio for duration and amplitude
+ _audioDuration = AudioAnalyzer.GetAudioDuration(_audioExport);
+ if (_audioDuration <= 0)
+ {
+ // Estimate duration from text if audio analysis fails
+ _audioDuration = EstimateDurationFromText(_tlkText);
+ }
+
+ // ALWAYS analyze audio amplitude - this is critical for natural lip sync
+ if (_audioExport != null)
+ {
+ _amplitudeData = AudioAnalyzer.AnalyzeAmplitude(_audioExport);
+ }
+ else
+ {
+ _amplitudeData = new List();
+ }
+
+ // Clear existing lip sync animations
+ ClearLipSyncAnimations();
+
+ // ALWAYS generate text-based phonemes - this is the foundation
+ List textPhonemes = null;
+ if (!string.IsNullOrWhiteSpace(_tlkText))
+ {
+ textPhonemes = TextToPhonemeAnalyzer.AnalyzeText(_tlkText, _audioDuration);
+ _phonemes = textPhonemes;
+ }
+
+ // Generate lip sync - use the working method
+ if (textPhonemes != null && textPhonemes.Count > 0)
+ {
+ // Use the proven GenerateLipSyncAnimations method which works
+ GenerateLipSyncAnimations(textPhonemes);
+ }
+ else
+ {
+ LastError = "No text provided for lip sync generation.";
+ return false;
+ }
+
+ // If we have FXA data, merge it in (it can enhance the generated animations)
+ if (_options.FxaData != null && _options.FxaData.Animations.Count > 0)
+ {
+ MergeFxaAnimations(_options.FxaData);
+ }
+
+ // Generate blink animation
+ if (_options.GenerateBlinkAnimation)
+ {
+ GenerateBlinkAnimation();
+ }
+
+ // Generate eyebrow animation for emphasis
+ if (_options.GenerateEyebrowAnimation)
+ {
+ GenerateEyebrowAnimation();
+ }
+
+ // Generate subtle head movement
+ if (_options.GenerateHeadMovement)
+ {
+ GenerateHeadMovement();
+ }
+
+ // Generate emotion expression
+ if (_options.Emotion != EmotionType.None && _options.EmotionIntensity > 0)
+ {
+ GenerateEmotionAnimation();
+ }
+
+ LastError = null;
+ return true;
+ }
+ catch (Exception ex)
+ {
+ LastError = ex.Message;
+ return false;
+ }
+ }
+
+ ///
+ /// Generate lip sync animations from phonemes, modulated by audio amplitude
+ ///
+ private void GenerateLipSyncAnimationsWithAudio(List phonemes)
+ {
+ if (phonemes == null || phonemes.Count == 0)
+ return;
+
+ // Get species-specific phoneme map
+ var phonemeMap = PhonemeToVisemeMap.GetPhonemeMap(_options.Species);
+
+ // Group phoneme events by viseme animation
+ var visemeAnimations = new Dictionary>();
+
+ // Maximum weight cap
+ const float MaxWeight = 1.0f;
+
+ foreach (var phoneme in phonemes)
+ {
+ if (!phonemeMap.TryGetValue(phoneme.Phoneme, out var mappings))
+ continue;
+
+ float centerTime = phoneme.StartTime + phoneme.Duration / 2f;
+
+ // Get audio amplitude at this time for modulation
+ float amplitudeModifier = GetAmplitudeAtTime(centerTime);
+
+ // Apply both intensity scale and amplitude modulation
+ // Ensure minimum modulation so animations are always visible
+ float intensityScale = _options.LipSyncIntensity * Math.Max(0.5f, amplitudeModifier);
+
+ foreach (var mapping in mappings)
+ {
+ if (!visemeAnimations.ContainsKey(mapping.VisemeName))
+ {
+ visemeAnimations[mapping.VisemeName] = new List<(float time, float weight)>();
+ }
+
+ float weight = Math.Min(mapping.Weight * intensityScale, MaxWeight);
+
+ // Only add the peak point - we'll interpolate the rest
+ visemeAnimations[mapping.VisemeName].Add((centerTime, weight));
+ }
+ }
+
+ // Convert to proper animation curves with smooth interpolation
+ foreach (var kvp in visemeAnimations)
+ {
+ var peakPoints = kvp.Value.OrderBy(p => p.time).ToList();
+
+ if (peakPoints.Count == 0)
+ continue;
+
+ var points = new List();
+
+ // Start at 0
+ points.Add(new FaceFXControlPoint
+ {
+ time = 0f,
+ weight = 0f,
+ inTangent = 0f,
+ leaveTangent = 0f
+ });
+
+ // Process each peak point with attack and release
+ float lastReleaseTime = 0f;
+
+ for (int i = 0; i < peakPoints.Count; i++)
+ {
+ var (peakTime, peakWeight) = peakPoints[i];
+
+ // Calculate attack time (before peak)
+ float attackDuration = 0.04f; // 40ms attack
+ float releaseDuration = 0.04f; // 40ms release
+
+ float attackTime = Math.Max(peakTime - attackDuration, lastReleaseTime + 0.01f);
+ float releaseTime = peakTime + releaseDuration;
+
+ // Check if we need to add a zero point before attack
+ if (attackTime > lastReleaseTime + 0.02f)
+ {
+ points.Add(new FaceFXControlPoint
+ {
+ time = attackTime - 0.01f,
+ weight = 0f,
+ inTangent = 0f,
+ leaveTangent = 0f
+ });
+ }
+
+ // Attack point (ramp up)
+ points.Add(new FaceFXControlPoint
+ {
+ time = attackTime,
+ weight = peakWeight * 0.3f,
+ inTangent = 0f,
+ leaveTangent = peakWeight * 5f
+ });
+
+ // Peak point
+ points.Add(new FaceFXControlPoint
+ {
+ time = peakTime,
+ weight = peakWeight,
+ inTangent = 0f,
+ leaveTangent = 0f
+ });
+
+ // Release point (ramp down) - only if not overlapping with next peak
+ bool hasNextPeak = i < peakPoints.Count - 1;
+ float nextPeakTime = hasNextPeak ? peakPoints[i + 1].time : float.MaxValue;
+
+ if (releaseTime < nextPeakTime - 0.05f)
+ {
+ points.Add(new FaceFXControlPoint
+ {
+ time = releaseTime,
+ weight = 0f,
+ inTangent = -peakWeight * 5f,
+ leaveTangent = 0f
+ });
+ lastReleaseTime = releaseTime;
+ }
+ else
+ {
+ lastReleaseTime = peakTime;
+ }
+ }
+
+ // End at 0
+ if (points.Count > 0 && points[^1].time < _audioDuration - 0.01f)
+ {
+ // Add final zero point
+ points.Add(new FaceFXControlPoint
+ {
+ time = Math.Min(lastReleaseTime + 0.05f, _audioDuration),
+ weight = 0f,
+ inTangent = 0f,
+ leaveTangent = 0f
+ });
+
+ if (points[^1].time < _audioDuration - 0.01f)
+ {
+ points.Add(new FaceFXControlPoint
+ {
+ time = _audioDuration,
+ weight = 0f,
+ inTangent = 0f,
+ leaveTangent = 0f
+ });
+ }
+ }
+
+ // Remove duplicate times (keep highest weight)
+ points = RemoveDuplicateTimes(points);
+
+ AddAnimation(kvp.Key, points);
+ }
+ }
+
+ ///
+ /// Remove points with duplicate times, keeping the highest weight
+ ///
+ private List RemoveDuplicateTimes(List points)
+ {
+ if (points.Count < 2)
+ return points;
+
+ var result = new List();
+ var sortedPoints = points.OrderBy(p => p.time).ToList();
+
+ FaceFXControlPoint current = sortedPoints[0];
+
+ for (int i = 1; i < sortedPoints.Count; i++)
+ {
+ var next = sortedPoints[i];
+
+ if (Math.Abs(next.time - current.time) < 0.005f)
+ {
+ // Same time - keep the one with higher weight
+ if (next.weight > current.weight)
+ {
+ current = next;
+ }
+ }
+ else
+ {
+ result.Add(current);
+ current = next;
+ }
+ }
+
+ result.Add(current);
+ return result;
+ }
+
+ ///
+ /// Merge FXA animation data - only replaces if FXA has meaningful data
+ ///
+ private void MergeFxaAnimations(FxaAnimationData fxaData)
+ {
+ if (fxaData == null) return;
+
+ foreach (var kvp in fxaData.Animations)
+ {
+ string animName = kvp.Key;
+ FxaAnimation fxaAnim = kvp.Value;
+
+ // Skip non-lip-sync animations or empty animations
+ if (!animName.StartsWith("m_") || fxaAnim.Keys.Count == 0)
+ continue;
+
+ // Check if the FXA animation has any meaningful (non-zero) values
+ bool hasNonZeroValues = fxaAnim.Keys.Any(k => Math.Abs(k.Value) > 0.01f);
+ if (!hasNonZeroValues)
+ {
+ // FXA has this animation but it's all zeros - skip it
+ // Keep the text-generated animation instead
+ continue;
+ }
+
+ // Check if we already have this animation
+ int existingIndex = -1;
+ for (int i = 0; i < _line.AnimationNames.Count; i++)
+ {
+ int nameIdx = _line.AnimationNames[i];
+ if (nameIdx >= 0 && nameIdx < _faceFX.Names.Count && _faceFX.Names[nameIdx] == animName)
+ {
+ existingIndex = i;
+ break;
+ }
+ }
+
+ // Convert FXA keys to control points
+ var points = new List();
+
+ float fxaMinTime = fxaAnim.Keys.Min(k => k.Time);
+ float fxaMaxTime = fxaAnim.Keys.Max(k => k.Time);
+ float fxaDuration = fxaMaxTime - fxaMinTime;
+ float timeScale = fxaDuration > 0.01f ? _audioDuration / fxaDuration : 1.0f;
+
+ foreach (var key in fxaAnim.Keys)
+ {
+ float scaledTime = (key.Time - fxaMinTime) * timeScale;
+ float scaledValue = key.Value * _options.LipSyncIntensity;
+
+ // Modulate with audio amplitude
+ float ampMod = GetAmplitudeAtTime(scaledTime);
+ scaledValue *= Math.Max(0.7f, ampMod); // At least 70% of the value
+
+ points.Add(new FaceFXControlPoint
+ {
+ time = scaledTime,
+ weight = scaledValue,
+ inTangent = key.InTangent,
+ leaveTangent = key.OutTangent
+ });
+ }
+
+ points = points.OrderBy(p => p.time).ToList();
+
+ // Final check: does the converted data have any significant values?
+ bool hasSignificantPoints = points.Any(p => Math.Abs(p.weight) > 0.02f);
+ if (!hasSignificantPoints)
+ {
+ // Even after conversion, no significant values - skip
+ continue;
+ }
+
+ // Ensure we have start and end points
+ if (points.Count > 0 && points[0].time > 0.01f)
+ {
+ points.Insert(0, new FaceFXControlPoint { time = 0f, weight = 0f, inTangent = 0f, leaveTangent = 0f });
+ }
+ if (points.Count > 0 && points[^1].time < _audioDuration - 0.01f)
+ {
+ points.Add(new FaceFXControlPoint { time = _audioDuration, weight = 0f, inTangent = 0f, leaveTangent = 0f });
+ }
+
+ if (existingIndex >= 0)
+ {
+ // Replace existing animation with FXA data (FXA takes priority)
+ ReplaceAnimation(existingIndex, points);
+ }
+ else
+ {
+ // Add new animation
+ AddAnimation(animName, points);
+ }
+ }
+ }
+
+ ///
+ /// Refine animation timing using phoneme events from FXT
+ ///
+ private void RefineWithPhonemeEvents(List<(string phoneme, float startTime, float endTime)> phonemeEvents)
+ {
+ // This adjusts the timing of existing animations based on precise phoneme timing
+ // For now, we'll add additional keyframes at phoneme boundaries
+
+ var phonemeMap = PhonemeToVisemeMap.GetPhonemeMap(_options.Species);
+
+ foreach (var (phoneme, startTime, endTime) in phonemeEvents)
+ {
+ if (!phonemeMap.TryGetValue(phoneme.ToUpper(), out var mappings))
+ continue;
+
+ float ampMod = GetAmplitudeAtTime((startTime + endTime) / 2f);
+ float intensity = _options.LipSyncIntensity * (0.5f + ampMod * 0.5f);
+
+ foreach (var mapping in mappings)
+ {
+ // Find this animation
+ int animIndex = -1;
+ for (int i = 0; i < _line.AnimationNames.Count; i++)
+ {
+ int nameIdx = _line.AnimationNames[i];
+ if (nameIdx >= 0 && nameIdx < _faceFX.Names.Count && _faceFX.Names[nameIdx] == mapping.VisemeName)
+ {
+ animIndex = i;
+ break;
+ }
+ }
+
+ if (animIndex >= 0)
+ {
+ // Add refinement keyframes
+ var newPoints = new List
+ {
+ new FaceFXControlPoint { time = startTime, weight = 0f, inTangent = 0f, leaveTangent = 0f },
+ new FaceFXControlPoint { time = (startTime + endTime) / 2f, weight = mapping.Weight * intensity, inTangent = 0f, leaveTangent = 0f },
+ new FaceFXControlPoint { time = endTime, weight = 0f, inTangent = 0f, leaveTangent = 0f }
+ };
+ AppendToAnimation(animIndex, newPoints);
+ }
+ }
+ }
+ }
+
+ ///
+ /// Replace an existing animation's points
+ ///
+ private void ReplaceAnimation(int animIndex, List newPoints)
+ {
+ // Calculate point offset
+ int pointOffset = 0;
+ for (int i = 0; i < animIndex; i++)
+ {
+ pointOffset += _line.NumKeys[i];
+ }
+
+ // Remove old points
+ int oldCount = _line.NumKeys[animIndex];
+ if (oldCount > 0 && pointOffset < _line.Points.Count)
+ {
+ _line.Points.RemoveRange(pointOffset, Math.Min(oldCount, _line.Points.Count - pointOffset));
+ }
+
+ // Insert new points
+ _line.Points.InsertRange(pointOffset, newPoints);
+ _line.NumKeys[animIndex] = newPoints.Count;
+ }
+
+ ///
+ /// Merge nearby control points to avoid jitter
+ ///
+ private List MergeNearbyPoints(List points, float threshold)
+ {
+ if (points.Count < 2) return points;
+
+ var result = new List { points[0] };
+
+ for (int i = 1; i < points.Count; i++)
+ {
+ var last = result[^1];
+ var current = points[i];
+
+ if (current.time - last.time < threshold)
+ {
+ // Merge by keeping the higher weight
+ if (current.weight > last.weight)
+ {
+ result[^1] = current;
+ }
+ }
+ else
+ {
+ result.Add(current);
+ }
+ }
+
+ return result;
+ }
+
+ ///
+ /// Get audio amplitude at a specific time (0-1 range) with interpolation
+ ///
+ private float GetAmplitudeAtTime(float time)
+ {
+ if (_amplitudeData == null || _amplitudeData.Count == 0)
+ return 1.0f; // Default to full intensity if no amplitude data
+
+ // Find the two closest samples for interpolation
+ AmplitudeData before = null;
+ AmplitudeData after = null;
+
+ foreach (var amp in _amplitudeData)
+ {
+ if (amp.Time <= time)
+ {
+ if (before == null || amp.Time > before.Time)
+ before = amp;
+ }
+ if (amp.Time >= time)
+ {
+ if (after == null || amp.Time < after.Time)
+ after = amp;
+ }
+ }
+
+ // Interpolate between the two samples
+ float amplitude;
+ if (before == null && after == null)
+ {
+ amplitude = 1.0f;
+ }
+ else if (before == null)
+ {
+ amplitude = after.NormalizedAmplitude;
+ }
+ else if (after == null)
+ {
+ amplitude = before.NormalizedAmplitude;
+ }
+ else if (Math.Abs(after.Time - before.Time) < 0.001f)
+ {
+ amplitude = before.NormalizedAmplitude;
+ }
+ else
+ {
+ // Linear interpolation
+ float t = (time - before.Time) / (after.Time - before.Time);
+ amplitude = before.NormalizedAmplitude + t * (after.NormalizedAmplitude - before.NormalizedAmplitude);
+ }
+
+ // Use full amplitude for maximum mouth movement
+ return Math.Max(0.8f, amplitude);
+ }
+
+ ///
+ /// Use text analysis to supplement FXA/FXT data - fill in missing animations
+ ///
+ private void SupplementWithTextAnalysis(List textPhonemes)
+ {
+ // Get the list of animation names that were already added
+ var existingAnims = new HashSet();
+ foreach (var animIndex in _line.AnimationNames)
+ {
+ if (animIndex >= 0 && animIndex < _faceFX.Names.Count)
+ {
+ existingAnims.Add(_faceFX.Names[animIndex]);
+ }
+ }
+
+ var phonemeMap = PhonemeToVisemeMap.GetPhonemeMap(_options.Species);
+
+ // Generate animations from text analysis for any missing visemes
+ foreach (var phoneme in textPhonemes)
+ {
+ if (!phonemeMap.TryGetValue(phoneme.Phoneme, out var mappings))
+ continue;
+
+ foreach (var mapping in mappings)
+ {
+ // Only add if this animation wasn't already added from FXA/FXT
+ if (!existingAnims.Contains(mapping.VisemeName))
+ {
+ // This viseme wasn't in the FXA/FXT data - add it from text analysis
+ // But use a reduced weight since it's supplementary
+ var points = new List
+ {
+ new FaceFXControlPoint { time = phoneme.StartTime, weight = 0f, inTangent = 0f, leaveTangent = 0f },
+ new FaceFXControlPoint { time = phoneme.StartTime + phoneme.Duration * 0.3f, weight = mapping.Weight * 0.5f * _options.LipSyncIntensity, inTangent = 0f, leaveTangent = 0f },
+ new FaceFXControlPoint { time = phoneme.StartTime + phoneme.Duration * 0.7f, weight = mapping.Weight * 0.5f * _options.LipSyncIntensity, inTangent = 0f, leaveTangent = 0f },
+ new FaceFXControlPoint { time = phoneme.StartTime + phoneme.Duration, weight = 0f, inTangent = 0f, leaveTangent = 0f }
+ };
+
+ // Check if we already have some data for this animation from a previous phoneme
+ int existingIndex = -1;
+ for (int i = 0; i < _line.AnimationNames.Count; i++)
+ {
+ int nameIdx = _line.AnimationNames[i];
+ if (nameIdx >= 0 && nameIdx < _faceFX.Names.Count && _faceFX.Names[nameIdx] == mapping.VisemeName)
+ {
+ existingIndex = i;
+ break;
+ }
+ }
+
+ if (existingIndex >= 0)
+ {
+ // Append to existing animation
+ AppendToAnimation(existingIndex, points);
+ }
+ else
+ {
+ // Create new animation
+ AddAnimation(mapping.VisemeName, points);
+ existingAnims.Add(mapping.VisemeName);
+ }
+ }
+ }
+ }
+ }
+
+ ///
+ /// Append points to an existing animation
+ ///
+ private void AppendToAnimation(int animIndex, List newPoints)
+ {
+ // Calculate the point offset for this animation
+ int pointOffset = 0;
+ for (int i = 0; i < animIndex; i++)
+ {
+ pointOffset += _line.NumKeys[i];
+ }
+
+ // Insert the new points at the correct position (sorted by time)
+ int insertIndex = pointOffset + _line.NumKeys[animIndex];
+
+ // Find correct insertion point based on time
+ for (int i = pointOffset; i < pointOffset + _line.NumKeys[animIndex]; i++)
+ {
+ if (i < _line.Points.Count && _line.Points[i].time > newPoints[0].time)
+ {
+ insertIndex = i;
+ break;
+ }
+ }
+
+ _line.Points.InsertRange(insertIndex, newPoints);
+ _line.NumKeys[animIndex] += newPoints.Count;
+ }
+
+ private float EstimateDurationFromText(string text)
+ {
+ if (string.IsNullOrWhiteSpace(text))
+ return 2.0f; // Default 2 seconds
+
+ // Count approximate syllables (simplified)
+ int vowelCount = text.Count(c => "aeiouAEIOU".Contains(c));
+ int wordCount = text.Split(new[] { ' ', '\t', '\n' }, StringSplitOptions.RemoveEmptyEntries).Length;
+
+
+ // Average speaking rate is about 150 words per minute
+ // Or about 4-5 syllables per second
+ float estimatedDuration = Math.Max(vowelCount * 0.15f, wordCount * 0.4f);
+ return Math.Max(0.5f, estimatedDuration); // Minimum 0.5 seconds
+ }
+
+ private void ClearLipSyncAnimations()
+ {
+ // Safety checks
+ if (_line.AnimationNames == null || _line.AnimationNames.Count == 0)
+ return;
+ if (_line.NumKeys == null || _line.Points == null)
+ return;
+
+ // Remove all m_ prefixed animations (lip sync) from the line
+ var indicesToRemove = new List();
+ for (int i = 0; i < _line.AnimationNames.Count; i++)
+ {
+ int nameIndex = _line.AnimationNames[i];
+ if (nameIndex >= 0 && nameIndex < _faceFX.Names.Count)
+ {
+ string animName = _faceFX.Names[nameIndex];
+ if (animName.StartsWith("m_"))
+ {
+ indicesToRemove.Add(i);
+ }
+ }
+ }
+
+ // Remove in reverse order to maintain indices
+ for (int i = indicesToRemove.Count - 1; i >= 0; i--)
+ {
+ int idx = indicesToRemove[i];
+
+ // Calculate point offset
+ int pointOffset = 0;
+ for (int j = 0; j < idx; j++)
+ {
+ pointOffset += _line.NumKeys[j];
+ }
+
+ // Remove points
+ int numPoints = _line.NumKeys[idx];
+ if (pointOffset >= 0 && numPoints > 0 && pointOffset + numPoints <= _line.Points.Count)
+ {
+ _line.Points.RemoveRange(pointOffset, numPoints);
+ }
+
+ // Remove animation
+ _line.AnimationNames.RemoveAt(idx);
+ _line.NumKeys.RemoveAt(idx);
+ }
+ }
+
+ ///
+ /// Import animation curves directly from FXA data
+ ///
+ private void ImportFxaAnimations(FxaAnimationData fxaData)
+ {
+ float duration = Math.Max(_audioDuration, 1.0f);
+ float intensityScale = _options.LipSyncIntensity;
+
+ foreach (var kvp in fxaData.Animations)
+ {
+ string animName = kvp.Key;
+ FxaAnimation fxaAnim = kvp.Value;
+
+ // Only import lip sync animations (m_ prefix)
+ if (!animName.StartsWith("m_"))
+ continue;
+
+ if (fxaAnim.Keys.Count == 0)
+ continue;
+
+ // Convert FXA keys to FaceFX control points
+ var points = new List();
+
+ // Get the time range of the FXA animation
+ float fxaMinTime = fxaAnim.Keys.Min(k => k.Time);
+ float fxaMaxTime = fxaAnim.Keys.Max(k => k.Time);
+ float fxaDuration = fxaMaxTime - fxaMinTime;
+
+ // Scale time to match our audio duration if needed
+ float timeScale = fxaDuration > 0.01f ? duration / fxaDuration : 1.0f;
+
+ foreach (var key in fxaAnim.Keys)
+ {
+ // Scale time to match audio duration
+ float scaledTime = (key.Time - fxaMinTime) * timeScale;
+
+ // Apply intensity scaling to the value
+ float scaledValue = key.Value * intensityScale;
+
+ points.Add(new FaceFXControlPoint
+ {
+ time = scaledTime,
+ weight = scaledValue,
+ inTangent = key.InTangent,
+ leaveTangent = key.OutTangent
+ });
+ }
+
+ // Ensure we have start and end points at 0
+ if (points.Count > 0)
+ {
+ if (points[0].time > 0.01f)
+ {
+ points.Insert(0, new FaceFXControlPoint { time = 0f, weight = 0f, inTangent = 0f, leaveTangent = 0f });
+ }
+ if (points[^1].time < duration - 0.01f)
+ {
+ points.Add(new FaceFXControlPoint { time = duration, weight = 0f, inTangent = 0f, leaveTangent = 0f });
+ }
+ }
+
+ // Sort by time
+ points = points.OrderBy(p => p.time).ToList();
+
+ AddAnimation(animName, points);
+ }
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ private void GenerateLipSyncAnimations(List phonemes)
+ {
+ float duration = Math.Max(_audioDuration, 1.0f);
+
+ // === CORRECT APPROACH ===
+ // Text determines WHICH animations (phonemes -> visemes)
+ // Audio determines TIMING, WIDTH, and STRENGTH
+
+ // Get species-specific mappings
+ var phonemeMap = PhonemeToVisemeMap.GetPhonemeMap(_options.Species);
+ var visemeNames = PhonemeToVisemeMap.GetVisemes(_options.Species);
+
+ // Step 1: Analyze audio to find speech segments and amplitude envelope
+ var audioSegments = AnalyzeAudioForSpeechSegments(duration);
+
+ // Step 2: Map text phonemes to audio timing
+ var timedPhonemes = MapPhonemesToAudioTiming(phonemes, audioSegments, duration);
+
+ // Step 3: Generate viseme curves based on timed phonemes
+ // Use a sampled curve approach for smoother animation
+ var visemeSamples = new Dictionary();
+
+ // Sample rate: 30 samples per second (smooth enough, not too dense)
+ const float sampleRate = 30f;
+ int numSamples = (int)(duration * sampleRate) + 1;
+
+ // Initialize all visemes with zero samples
+ foreach (var viseme in visemeNames)
+ {
+ visemeSamples[viseme] = new float[numSamples];
+ }
+
+ // Process each timed phoneme - add contribution to the sampled curves
+ foreach (var timedPhoneme in timedPhonemes)
+ {
+ if (!phonemeMap.TryGetValue(timedPhoneme.Phoneme, out var mappings))
+ continue;
+
+ float peakTime = timedPhoneme.StartTime + timedPhoneme.Duration * 0.5f;
+ float intensity = timedPhoneme.Intensity;
+
+ // Calculate envelope parameters - faster attack/release for snappier animation
+ float attackStart = timedPhoneme.StartTime;
+ float attackEnd = timedPhoneme.StartTime + timedPhoneme.Duration * 0.25f;
+ float releaseStart = timedPhoneme.StartTime + timedPhoneme.Duration * 0.75f;
+ float releaseEnd = timedPhoneme.StartTime + timedPhoneme.Duration;
+
+ // Add contribution to each mapped viseme
+ foreach (var mapping in mappings)
+ {
+ if (!visemeSamples.ContainsKey(mapping.VisemeName))
+ continue;
+
+ float[] samples = visemeSamples[mapping.VisemeName];
+
+ // Full weight for proper mouth opening
+ float peakWeight = mapping.Weight * intensity * _options.LipSyncIntensity;
+ peakWeight = Math.Min(peakWeight, 1.0f);
+
+ if (peakWeight < 0.02f)
+ continue;
+
+ // Add this phoneme's contribution to the curve using a smooth envelope
+ for (int i = 0; i < numSamples; i++)
+ {
+ float t = i / sampleRate;
+
+ if (t < attackStart || t > releaseEnd)
+ continue;
+
+ float weight = 0f;
+
+ if (t < attackEnd)
+ {
+ // Attack phase - smooth ramp up
+ float attackT = (t - attackStart) / (attackEnd - attackStart);
+ weight = peakWeight * SmoothStep(attackT);
+ }
+ else if (t < releaseStart)
+ {
+ // Sustain phase - hold at peak
+ weight = peakWeight;
+ }
+ else
+ {
+ // Release phase - smooth ramp down
+ float releaseT = (t - releaseStart) / (releaseEnd - releaseStart);
+ weight = peakWeight * (1f - SmoothStep(releaseT));
+ }
+
+ // Use max blending (like FaceFX does)
+ samples[i] = Math.Max(samples[i], weight);
+ }
+ }
+ }
+
+ // Convert sampled curves to keyframes (with intelligent decimation)
+ foreach (var viseme in visemeNames)
+ {
+ var samples = visemeSamples[viseme];
+
+ // Apply smoothing to reduce jitter - more smoothing for m_Open which gets many triggers
+ int smoothingPasses = viseme == "m_Open" ? 3 : 1;
+ for (int pass = 0; pass < smoothingPasses; pass++)
+ {
+ SmoothSamplesInPlace(samples);
+ }
+
+ var keyframes = ConvertSamplesToKeyframes(samples, sampleRate, duration);
+
+ if (keyframes.Count >= 2)
+ {
+ AddAnimation(viseme, keyframes);
+ }
+ }
+ }
+
+ ///
+ /// Smooth samples in-place using a 3-point moving average
+ ///
+ private void SmoothSamplesInPlace(float[] samples)
+ {
+ if (samples.Length < 3)
+ return;
+
+ float prev = samples[0];
+ for (int i = 1; i < samples.Length - 1; i++)
+ {
+ float current = samples[i];
+ float next = samples[i + 1];
+ samples[i] = (prev + current + next) / 3f;
+ prev = current;
+ }
+ }
+
+ ///
+ /// Smooth step function for smooth attack/release
+ ///
+ private float SmoothStep(float t)
+ {
+ t = Math.Clamp(t, 0f, 1f);
+ return t * t * (3f - 2f * t);
+ }
+
+ ///
+ /// Convert sampled curve to keyframes with intelligent decimation
+ /// Only keeps keyframes at significant changes (peaks, valleys, inflection points)
+ ///
+ private List ConvertSamplesToKeyframes(float[] samples, float sampleRate, float duration)
+ {
+ var keyframes = new List();
+
+ if (samples.Length < 2)
+ {
+ keyframes.Add(new FaceFXControlPoint { time = 0f, weight = 0f, inTangent = 0f, leaveTangent = 0f });
+ keyframes.Add(new FaceFXControlPoint { time = duration, weight = 0f, inTangent = 0f, leaveTangent = 0f });
+ return keyframes;
+ }
+
+ // Always start at 0
+ keyframes.Add(new FaceFXControlPoint { time = 0f, weight = samples[0], inTangent = 0f, leaveTangent = 0f });
+
+ // Faster keyframe interval for snappier animation
+ const float minKeyframeInterval = 0.08f; // 80ms between keyframes
+ const float significanceThreshold = 0.02f; // Lower threshold for more responsive animation
+
+ float lastKeyframeTime = 0f;
+ float lastKeyframeWeight = samples[0];
+
+ for (int i = 1; i < samples.Length - 1; i++)
+ {
+ float time = i / sampleRate;
+ float weight = samples[i];
+ float prevWeight = samples[i - 1];
+ float nextWeight = samples[i + 1];
+
+ // Check if this is a significant point
+ bool isPeak = prevWeight < weight && weight > nextWeight && weight > 0.015f;
+ bool isValley = prevWeight > weight && weight < nextWeight && lastKeyframeWeight > 0.015f;
+ bool isSignificantChange = Math.Abs(weight - lastKeyframeWeight) > significanceThreshold;
+ bool hasEnoughTime = time - lastKeyframeTime >= minKeyframeInterval;
+
+ if (hasEnoughTime && (isPeak || isValley || isSignificantChange))
+ {
+ keyframes.Add(new FaceFXControlPoint
+ {
+ time = time,
+ weight = weight,
+ inTangent = 0f,
+ leaveTangent = 0f
+ });
+ lastKeyframeTime = time;
+ lastKeyframeWeight = weight;
+ }
+ }
+
+ // Always end at 0
+ keyframes.Add(new FaceFXControlPoint { time = duration, weight = samples[^1], inTangent = 0f, leaveTangent = 0f });
+
+ return keyframes;
+ }
+
+ ///
+ /// Analyze audio to find speech segments with amplitude information
+ ///
+ private List AnalyzeAudioForSpeechSegments(float duration)
+ {
+ var segments = new List();
+
+ if (_amplitudeData == null || _amplitudeData.Count < 2)
+ {
+ // No audio data - create one segment spanning the whole duration
+ segments.Add(new AudioSegment
+ {
+ StartTime = 0f,
+ EndTime = duration,
+ PeakAmplitude = 0.8f,
+ AverageAmplitude = 0.8f
+ });
+ return segments;
+ }
+
+ // Find speech segments based on amplitude threshold
+ const float speechThreshold = 0.15f; // Minimum amplitude to consider as speech
+ const float minSegmentDuration = 0.1f; // Minimum 100ms segment
+ const float minGapToSplit = 0.15f; // Minimum 150ms silence to split segments
+
+ bool inSpeech = false;
+ float segmentStart = 0f;
+ float peakAmp = 0f;
+ float sumAmp = 0f;
+ int ampCount = 0;
+ float lastSpeechTime = 0f;
+
+ foreach (var amp in _amplitudeData)
+ {
+ if (!inSpeech && amp.NormalizedAmplitude > speechThreshold)
+ {
+ // Start of speech segment
+ inSpeech = true;
+ segmentStart = amp.Time;
+ peakAmp = amp.NormalizedAmplitude;
+ sumAmp = amp.NormalizedAmplitude;
+ ampCount = 1;
+ lastSpeechTime = amp.Time;
+ }
+ else if (inSpeech)
+ {
+ if (amp.NormalizedAmplitude > speechThreshold)
+ {
+ // Continue speech segment
+ peakAmp = Math.Max(peakAmp, amp.NormalizedAmplitude);
+ sumAmp += amp.NormalizedAmplitude;
+ ampCount++;
+ lastSpeechTime = amp.Time;
+ }
+ else if (amp.Time - lastSpeechTime >= minGapToSplit)
+ {
+ // End of speech segment (long enough silence)
+ if (lastSpeechTime - segmentStart >= minSegmentDuration)
+ {
+ segments.Add(new AudioSegment
+ {
+ StartTime = segmentStart,
+ EndTime = lastSpeechTime,
+ PeakAmplitude = peakAmp,
+ AverageAmplitude = ampCount > 0 ? sumAmp / ampCount : peakAmp
+ });
+ }
+ inSpeech = false;
+ }
+ }
+ }
+
+ // Close final segment if still in speech
+ if (inSpeech && ampCount > 0 && lastSpeechTime - segmentStart >= minSegmentDuration)
+ {
+ segments.Add(new AudioSegment
+ {
+ StartTime = segmentStart,
+ EndTime = Math.Min(lastSpeechTime + 0.05f, duration),
+ PeakAmplitude = peakAmp,
+ AverageAmplitude = sumAmp / ampCount
+ });
+ }
+
+ // If no segments found, create one spanning the whole duration
+ if (segments.Count == 0)
+ {
+ segments.Add(new AudioSegment
+ {
+ StartTime = 0f,
+ EndTime = duration,
+ PeakAmplitude = 0.8f,
+ AverageAmplitude = 0.8f
+ });
+ }
+
+ return segments;
+ }
+
+ ///
+ /// Map text phonemes to audio timing based on speech segments
+ ///
+ private List MapPhonemesToAudioTiming(List phonemes, List audioSegments, float duration)
+ {
+ var timedPhonemes = new List();
+
+ if (phonemes.Count == 0)
+ return timedPhonemes;
+
+ // Calculate total speech duration from audio segments
+ float totalSpeechTime = audioSegments.Sum(s => s.EndTime - s.StartTime);
+
+ // Calculate average phoneme duration based on audio
+ // Slightly faster range for snappier animation
+ float avgPhonemeDuration = totalSpeechTime / phonemes.Count;
+ avgPhonemeDuration = Math.Max(0.06f, Math.Min(avgPhonemeDuration, 0.15f)); // Clamp to 60-150ms range
+
+ // Distribute phonemes across audio segments
+ int phonemeIndex = 0;
+
+ foreach (var segment in audioSegments)
+ {
+ float segmentDuration = segment.EndTime - segment.StartTime;
+ int phonemesInSegment = (int)Math.Round(segmentDuration / avgPhonemeDuration);
+ phonemesInSegment = Math.Max(1, Math.Min(phonemesInSegment, phonemes.Count - phonemeIndex));
+
+ float phonemeDuration = segmentDuration / phonemesInSegment;
+ // Ensure minimum duration - slightly faster
+ phonemeDuration = Math.Max(phonemeDuration, 0.06f);
+
+ for (int i = 0; i < phonemesInSegment && phonemeIndex < phonemes.Count; i++)
+ {
+ var phoneme = phonemes[phonemeIndex];
+
+ // Get local amplitude at this position
+ float localTime = segment.StartTime + i * phonemeDuration + phonemeDuration * 0.5f;
+ float localAmplitude = GetAmplitudeAtTime(localTime);
+
+ timedPhonemes.Add(new TimedPhoneme
+ {
+ Phoneme = phoneme.Phoneme,
+ StartTime = segment.StartTime + i * phonemeDuration,
+ Duration = phonemeDuration,
+ Intensity = Math.Max(0.9f, localAmplitude) // High minimum intensity for full mouth movement
+ });
+
+ phonemeIndex++;
+ }
+ }
+
+ // If we have remaining phonemes, distribute them evenly at the end
+ if (phonemeIndex < phonemes.Count)
+ {
+ float remainingTime = duration - (audioSegments.Count > 0 ? audioSegments[^1].EndTime : 0f);
+ if (remainingTime > 0.1f)
+ {
+ int remaining = phonemes.Count - phonemeIndex;
+ float startTime = audioSegments.Count > 0 ? audioSegments[^1].EndTime : 0f;
+ float phonemeDuration = Math.Max(remainingTime / remaining, 0.08f);
+
+ for (int i = phonemeIndex; i < phonemes.Count; i++)
+ {
+ timedPhonemes.Add(new TimedPhoneme
+ {
+ Phoneme = phonemes[i].Phoneme,
+ StartTime = startTime + (i - phonemeIndex) * phonemeDuration,
+ Duration = phonemeDuration,
+ Intensity = 1.0f // Full intensity
+ });
+ }
+ }
+ }
+
+ return timedPhonemes;
+ }
+
+ ///
+ /// Clean up keyframes by removing duplicates and merging very close points
+ ///
+ private List CleanupKeyframes(List keyframes)
+ {
+ if (keyframes.Count < 2)
+ return keyframes;
+
+ var result = new List();
+ const float minTimeDiff = 0.05f; // 50ms minimum between keyframes for smooth animation
+
+ foreach (var kf in keyframes)
+ {
+ if (result.Count == 0)
+ {
+ result.Add(kf);
+ continue;
+ }
+
+ var last = result[^1];
+
+ if (kf.time - last.time < minTimeDiff)
+ {
+ // Too close - keep the one with higher weight
+ if (kf.weight > last.weight)
+ {
+ result[^1] = kf;
+ }
+ }
+ else
+ {
+ result.Add(kf);
+ }
+ }
+
+ return result;
+ }
+
+ ///
+ /// Audio segment with amplitude information
+ ///
+ private class AudioSegment
+ {
+ public float StartTime { get; set; }
+ public float EndTime { get; set; }
+ public float PeakAmplitude { get; set; }
+ public float AverageAmplitude { get; set; }
+ }
+
+ ///
+ /// Phoneme with timing derived from audio
+ ///
+ private class TimedPhoneme
+ {
+ public string Phoneme { get; set; }
+ public float StartTime { get; set; }
+ public float Duration { get; set; }
+ public float Intensity { get; set; }
+ }
+
+ ///
+ /// Smooths keyframes by merging points that are too close together
+ ///
+ private List SmoothKeyframes(List points, float minInterval)
+ {
+ if (points.Count < 2)
+ return points;
+
+ var result = new List { points[0] };
+
+ for (int i = 1; i < points.Count; i++)
+ {
+ var lastPoint = result[result.Count - 1];
+ var currentPoint = points[i];
+
+ if (currentPoint.time - lastPoint.time < minInterval)
+ {
+ // Merge: keep the higher weight, use average time
+ if (currentPoint.weight > lastPoint.weight)
+ {
+ result[result.Count - 1] = new FaceFXControlPoint
+ {
+ time = (lastPoint.time + currentPoint.time) / 2f,
+ weight = currentPoint.weight,
+ inTangent = 0f,
+ leaveTangent = 0f
+ };
+ }
+ }
+ else
+ {
+ result.Add(currentPoint);
+ }
+ }
+
+ return result;
+ }
+
+
+
+
+
+
+
+
+
+
+
+ private void GenerateJawAnimation()
+ {
+ var points = new List();
+ points.Add(new FaceFXControlPoint { time = 0f, weight = 0f, inTangent = 0f, leaveTangent = 0f });
+
+ float duration = Math.Max(_audioDuration, 1.0f);
+
+ if (_phonemes != null && _phonemes.Count > 0)
+ {
+ // Phonemes that require significant jaw opening
+ var wideOpenPhonemes = new HashSet { "AA", "AE", "AH", "AO", "AW", "AY" };
+ var mediumOpenPhonemes = new HashSet { "EH", "EY", "OW", "OY", "H", "L", "R" };
+ var slightOpenPhonemes = new HashSet { "IH", "IY", "UH", "UW", "ER", "W", "Y" };
+
+ foreach (var phoneme in _phonemes)
+ {
+ if (phoneme.StartTime < 0.01f)
+ continue;
+
+ float weight = 0f;
+ float centerTime = phoneme.StartTime + phoneme.Duration * 0.5f;
+
+ if (wideOpenPhonemes.Contains(phoneme.Phoneme))
+ weight = 0.7f * _options.LipSyncIntensity;
+ else if (mediumOpenPhonemes.Contains(phoneme.Phoneme))
+ weight = 0.5f * _options.LipSyncIntensity;
+ else if (slightOpenPhonemes.Contains(phoneme.Phoneme))
+ weight = 0.3f * _options.LipSyncIntensity;
+ else
+ weight = 0.15f * _options.LipSyncIntensity;
+
+ // Single keyframe at center
+ points.Add(new FaceFXControlPoint { time = centerTime, weight = weight, inTangent = 0f, leaveTangent = 0f });
+ }
+ }
+ else
+ {
+ // Fallback: procedural animation
+ var random = new Random(42);
+ float interval = 0.15f;
+ float currentTime = interval;
+
+ while (currentTime < duration)
+ {
+ float weight = (0.2f + (float)random.NextDouble() * 0.4f) * _options.LipSyncIntensity;
+ points.Add(new FaceFXControlPoint { time = currentTime, weight = weight, inTangent = 0f, leaveTangent = 0f });
+ currentTime += interval;
+ }
+ }
+
+ points.Add(new FaceFXControlPoint { time = duration, weight = 0f, inTangent = 0f, leaveTangent = 0f });
+
+ var sortedPoints = points.OrderBy(p => p.time).ToList();
+ var smoothedPoints = SmoothKeyframes(sortedPoints, 0.05f);
+ AddAnimation("m_Open", smoothedPoints);
+ }
+
+ ///
+ /// Generates m_Jaw+ and m_Jaw- animations for jaw positioning during speech
+ /// Uses phoneme data to create natural jaw movement patterns
+ ///
+ private void GenerateJawPositionAnimations()
+ {
+ float duration = Math.Max(_audioDuration, 1.0f);
+
+ var jawUpPoints = new List();
+ var jawDownPoints = new List();
+
+ jawUpPoints.Add(new FaceFXControlPoint { time = 0f, weight = 0f, inTangent = 0f, leaveTangent = 0f });
+ jawDownPoints.Add(new FaceFXControlPoint { time = 0f, weight = 0f, inTangent = 0f, leaveTangent = 0f });
+
+ if (_phonemes != null && _phonemes.Count > 0)
+ {
+ // Jaw down (m_Jaw-): vowels that need open mouth
+ var jawDownPhonemes = new HashSet { "AA", "AE", "AH", "AO", "AW", "AY", "EH", "OW", "OY", "H" };
+ // Jaw up (m_Jaw+): consonants that close the mouth
+ var jawUpPhonemes = new HashSet { "M", "P", "B", "F", "V", "TH", "DH", "S", "Z", "SH", "ZH", "CH", "JH" };
+
+ foreach (var phoneme in _phonemes)
+ {
+ if (phoneme.StartTime < 0.01f)
+ continue;
+
+ float weight = 0.4f * _options.LipSyncIntensity;
+ float centerTime = phoneme.StartTime + phoneme.Duration * 0.5f;
+
+ if (jawDownPhonemes.Contains(phoneme.Phoneme))
+ {
+ // Jaw opens down - single keyframe
+ jawDownPoints.Add(new FaceFXControlPoint { time = centerTime, weight = weight, inTangent = 0f, leaveTangent = 0f });
+ }
+ else if (jawUpPhonemes.Contains(phoneme.Phoneme))
+ {
+ // Jaw closes up - single keyframe
+ jawUpPoints.Add(new FaceFXControlPoint { time = centerTime, weight = weight, inTangent = 0f, leaveTangent = 0f });
+ }
+ else
+ {
+ // Neutral phonemes - subtle movement
+ float subtleWeight = 0.1f * _options.LipSyncIntensity;
+ jawUpPoints.Add(new FaceFXControlPoint { time = centerTime, weight = subtleWeight, inTangent = 0f, leaveTangent = 0f });
+ jawDownPoints.Add(new FaceFXControlPoint { time = centerTime, weight = subtleWeight, inTangent = 0f, leaveTangent = 0f });
+ }
+ }
+ }
+ else
+ {
+ // Fallback: procedural animation
+ var random = new Random(567);
+ float interval = 0.15f;
+ float currentTime = interval;
+ bool jawUp = random.NextDouble() > 0.5;
+
+
+ while (currentTime < duration)
+ {
+ float upWeight = jawUp ? (0.2f + (float)random.NextDouble() * 0.3f) * _options.LipSyncIntensity : 0.05f;
+ float downWeight = !jawUp ? (0.2f + (float)random.NextDouble() * 0.3f) * _options.LipSyncIntensity : 0.05f;
+
+ jawUpPoints.Add(new FaceFXControlPoint { time = currentTime, weight = upWeight, inTangent = 0f, leaveTangent = 0f });
+ jawDownPoints.Add(new FaceFXControlPoint { time = currentTime, weight = downWeight, inTangent = 0f, leaveTangent = 0f });
+
+ if (random.NextDouble() > 0.6)
+ jawUp = !jawUp;
+
+ currentTime += interval;
+ }
+ }
+
+ jawUpPoints.Add(new FaceFXControlPoint { time = duration, weight = 0f, inTangent = 0f, leaveTangent = 0f });
+ jawDownPoints.Add(new FaceFXControlPoint { time = duration, weight = 0f, inTangent = 0f, leaveTangent = 0f });
+
+ var sortedUp = jawUpPoints.OrderBy(p => p.time).ToList();
+ var sortedDown = jawDownPoints.OrderBy(p => p.time).ToList();
+
+ // Apply smoothing to merge close keyframes
+ var smoothedUp = SmoothKeyframes(sortedUp, 0.05f);
+ var smoothedDown = SmoothKeyframes(sortedDown, 0.05f);
+
+ AddAnimation("m_Jaw+", smoothedUp);
+ AddAnimation("m_Jaw-", smoothedDown);
+ }
+
+ private void GenerateBlinkAnimation()
+ {
+ var points = new List();
+ points.Add(new FaceFXControlPoint { time = 0f, weight = 0f, inTangent = 0f, leaveTangent = 0f });
+
+ float duration = Math.Max(_audioDuration, 1.0f);
+
+ // Generate blinks at random intervals
+ float averageBlinkInterval = 1f / _options.BlinkFrequency;
+ var random = new Random(123);
+ float currentTime = (float)(random.NextDouble() * averageBlinkInterval * 0.5 + 0.5);
+
+ while (currentTime < duration - 0.2f)
+ {
+ // Blink - single peak keyframe at center
+ float blinkDuration = 0.15f + (float)random.NextDouble() * 0.1f;
+ float peakTime = currentTime + blinkDuration * 0.5f;
+
+ points.Add(new FaceFXControlPoint { time = peakTime, weight = 1f, inTangent = 0f, leaveTangent = 0f });
+
+ // Next blink with some randomness
+ currentTime += averageBlinkInterval * (0.7f + (float)random.NextDouble() * 0.6f);
+ }
+
+ points.Add(new FaceFXControlPoint { time = duration, weight = 0f, inTangent = 0f, leaveTangent = 0f });
+
+ var sortedPoints = points.OrderBy(p => p.time).ToList();
+ AddAnimation("Blink", sortedPoints);
+ }
+
+ private void GenerateEyebrowAnimation()
+ {
+ var points = new List();
+ points.Add(new FaceFXControlPoint { time = 0f, weight = 0f, inTangent = 0f, leaveTangent = 0f });
+
+ float duration = Math.Max(_audioDuration, 1.0f);
+
+ // Use phoneme data to raise eyebrows on emphasized sounds
+ if (_phonemes != null && _phonemes.Count > 0)
+ {
+ var emphasisPhonemes = new HashSet { "AY", "EY", "OW", "IY", "UW", "AO", "H" };
+
+ int phonemeIndex = 0;
+ foreach (var phoneme in _phonemes)
+ {
+ if (phoneme.StartTime < 0.01f)
+ {
+ phonemeIndex++;
+ continue;
+ }
+
+ bool isLateInPhrase = phonemeIndex > _phonemes.Count * 0.6f;
+ bool isEmphasis = emphasisPhonemes.Contains(phoneme.Phoneme);
+
+ if (isEmphasis && (isLateInPhrase || phonemeIndex % 5 == 0))
+ {
+ float weight = 0.4f * _options.LipSyncIntensity;
+ float centerTime = phoneme.StartTime + phoneme.Duration * 0.5f;
+
+ // Single keyframe
+ points.Add(new FaceFXControlPoint { time = centerTime, weight = weight, inTangent = 0f, leaveTangent = 0f });
+ }
+
+ phonemeIndex++;
+ }
+ }
+ else
+ {
+ // Fallback: procedural animation - sparse keyframes
+ var random = new Random(789);
+ float currentTime = 1.0f + (float)random.NextDouble() * 1.0f;
+
+ while (currentTime < duration - 0.5f)
+ {
+ float weight = (0.3f + (float)random.NextDouble() * 0.3f) * _options.LipSyncIntensity;
+ points.Add(new FaceFXControlPoint { time = currentTime, weight = weight, inTangent = 0f, leaveTangent = 0f });
+ currentTime += 2.0f + (float)random.NextDouble() * 2.0f;
+ }
+ }
+
+ points.Add(new FaceFXControlPoint { time = duration, weight = 0f, inTangent = 0f, leaveTangent = 0f });
+
+ var sortedPoints = points.OrderBy(p => p.time).ToList();
+ AddAnimation("Eyebrow_Raise", sortedPoints);
+ }
+
+ private void GenerateHeadMovement()
+ {
+ float duration = Math.Max(_audioDuration, 1.0f);
+ var random = new Random(456);
+
+ foreach (var axis in new[] { "Emphasis_Head_Pitch", "Emphasis_Head_Yaw" })
+ {
+ var points = new List();
+ points.Add(new FaceFXControlPoint { time = 0f, weight = 0f, inTangent = 0f, leaveTangent = 0f });
+
+ // Use phonemes to drive head movement - sparse single keyframes
+ if (_phonemes != null && _phonemes.Count > 0)
+ {
+ var emphasisPhonemes = new HashSet { "AA", "AE", "AO", "AY", "EY", "OW", "H" };
+
+ foreach (var phoneme in _phonemes)
+ {
+ if (phoneme.StartTime < 0.01f)
+ continue;
+
+ if (emphasisPhonemes.Contains(phoneme.Phoneme) && random.NextDouble() > 0.7) // Less frequent
+ {
+ float weight = ((float)random.NextDouble() * 0.2f - 0.1f) * _options.LipSyncIntensity;
+ float centerTime = phoneme.StartTime + phoneme.Duration * 0.5f;
+ points.Add(new FaceFXControlPoint { time = centerTime, weight = weight, inTangent = 0f, leaveTangent = 0f });
+ }
+ }
+ }
+ else
+ {
+ // Fallback: sparse procedural
+ float interval = 0.6f + (float)random.NextDouble() * 0.4f;
+ float currentTime = interval;
+
+ while (currentTime < duration - 0.3f)
+ {
+ float weight = ((float)random.NextDouble() * 0.2f - 0.1f) * _options.LipSyncIntensity;
+ points.Add(new FaceFXControlPoint { time = currentTime, weight = weight, inTangent = 0f, leaveTangent = 0f });
+ currentTime += interval + (float)random.NextDouble() * 0.4f;
+ }
+ }
+
+ points.Add(new FaceFXControlPoint { time = duration, weight = 0f, inTangent = 0f, leaveTangent = 0f });
+
+
+ var sortedPoints = points.OrderBy(p => p.time).ToList();
+ AddAnimation(axis, sortedPoints);
+ }
+ }
+
+ ///
+ /// Generates emotion expression animations
+ ///
+ private void GenerateEmotionAnimation()
+ {
+ float duration = Math.Max(_audioDuration, 1.0f);
+ float intensity = _options.EmotionIntensity;
+
+ // Define emotion animation mappings
+ // Each emotion maps to specific facial animations with weights
+ var emotionMappings = GetEmotionMappings(_options.Emotion);
+
+ foreach (var mapping in emotionMappings)
+ {
+ var points = new List();
+
+ // Ease in at the start
+ points.Add(new FaceFXControlPoint
+ {
+ time = 0f,
+ weight = 0f,
+ inTangent = 0f,
+ leaveTangent = 0f
+ });
+
+ // Ramp up to full intensity
+ float rampUpTime = Math.Min(0.3f, duration * 0.1f);
+ points.Add(new FaceFXControlPoint
+ {
+ time = rampUpTime,
+ weight = mapping.Weight * intensity,
+ inTangent = 0f,
+ leaveTangent = 0f
+ });
+
+ // Hold emotion throughout with slight variation for naturalness
+ float holdTime = duration - 0.3f;
+ if (holdTime > rampUpTime + 0.5f)
+ {
+ // Add some variation in the middle for naturalness
+ float midTime = (rampUpTime + holdTime) / 2f;
+ float variation = 0.9f + (float)new Random().NextDouble() * 0.2f; // 90-110%
+ points.Add(new FaceFXControlPoint
+ {
+ time = midTime,
+ weight = mapping.Weight * intensity * variation,
+ inTangent = 0f,
+ leaveTangent = 0f
+ });
+ }
+
+ // Hold near the end
+ points.Add(new FaceFXControlPoint
+ {
+ time = Math.Max(holdTime, rampUpTime + 0.1f),
+ weight = mapping.Weight * intensity,
+ inTangent = 0f,
+ leaveTangent = 0f
+ });
+
+ // Ease out at the end
+ points.Add(new FaceFXControlPoint
+ {
+ time = duration,
+ weight = 0f,
+ inTangent = 0f,
+ leaveTangent = 0f
+ });
+
+ AddAnimation(mapping.AnimationName, points);
+ }
+ }
+
+
+
+
+
+
+ ///
+ /// Gets the animation mappings for a specific emotion
+ /// Uses actual Mass Effect FaceFX emotion animation names
+ /// Animation naming pattern: E_[Category]_[Emotion][Number]
+ /// Categories: S=Smile/Mouth, B=Brow, Y=Eye, WB=Wide Brow/Full Face, D=Other, D_S=Other Mouth
+ /// Each emotion should have variants for all face parts for complete expression
+ ///
+ private List GetEmotionMappings(EmotionType emotion)
+ {
+ var mappings = new List();
+
+ // Get available emotion animation names from the FaceFX asset
+ var availableEmotionAnims = _faceFX.Names
+ .Where(n => n.StartsWith("E_") || n.Contains("Blink") || n.Contains("Eyebrow"))
+ .ToHashSet();
+
+ switch (emotion)
+ {
+ case EmotionType.Anger:
+ // Anger - strong weights for game visibility
+ mappings.Add(new EmotionAnimationMapping("E_S_Anger1", 1.2f));
+ mappings.Add(new EmotionAnimationMapping("E_S_Anger2", 1.0f));
+ mappings.Add(new EmotionAnimationMapping("E_B_Anger1", 1.2f));
+ mappings.Add(new EmotionAnimationMapping("E_B_Anger2", 1.0f));
+ mappings.Add(new EmotionAnimationMapping("E_Y_Anger1", 1.2f));
+ mappings.Add(new EmotionAnimationMapping("E_Y_Anger2", 1.0f));
+ break;
+
+ case EmotionType.Disgust:
+ // Disgust - strong weights for game visibility
+ mappings.Add(new EmotionAnimationMapping("E_S_Disgust1", 1.2f));
+ mappings.Add(new EmotionAnimationMapping("E_S_Disgust2", 1.0f));
+ mappings.Add(new EmotionAnimationMapping("E_B_Disgust1", 1.2f));
+ mappings.Add(new EmotionAnimationMapping("E_B_Disgust2", 1.0f));
+ mappings.Add(new EmotionAnimationMapping("E_Y_Disgust1", 1.2f));
+ mappings.Add(new EmotionAnimationMapping("E_Y_Disgust2", 1.0f));
+ break;
+
+ case EmotionType.Fear:
+ // Fear - strong weights for game visibility
+ mappings.Add(new EmotionAnimationMapping("E_S_Fear1", 1.2f));
+ mappings.Add(new EmotionAnimationMapping("E_S_Fear2", 1.0f));
+ mappings.Add(new EmotionAnimationMapping("E_B_Fear1", 1.2f));
+ mappings.Add(new EmotionAnimationMapping("E_B_Fear2", 1.0f));
+ mappings.Add(new EmotionAnimationMapping("E_Y_Fear1", 1.2f));
+ mappings.Add(new EmotionAnimationMapping("E_Y_Fear2", 1.0f));
+ break;
+
+ case EmotionType.Happy:
+ // Happy/Joy - strong weights for game visibility
+ mappings.Add(new EmotionAnimationMapping("E_S_Joy1", 1.2f));
+ mappings.Add(new EmotionAnimationMapping("E_S_Joy2", 1.0f));
+ mappings.Add(new EmotionAnimationMapping("E_B_Joy1", 1.2f));
+ mappings.Add(new EmotionAnimationMapping("E_B_Joy2", 1.0f));
+ mappings.Add(new EmotionAnimationMapping("E_Y_Joy1", 1.2f));
+ mappings.Add(new EmotionAnimationMapping("E_Y_Joy2", 1.0f));
+ break;
+
+ case EmotionType.Sad:
+ // Sad - strong weights for game visibility
+ mappings.Add(new EmotionAnimationMapping("E_S_Sadness1", 1.2f));
+ mappings.Add(new EmotionAnimationMapping("E_S_Sadness2", 1.0f));
+ mappings.Add(new EmotionAnimationMapping("E_B_Sadness1", 1.2f));
+ mappings.Add(new EmotionAnimationMapping("E_B_Sadness2", 1.0f));
+ mappings.Add(new EmotionAnimationMapping("E_Y_Sadness1", 1.2f));
+ mappings.Add(new EmotionAnimationMapping("E_Y_Sadness2", 1.0f));
+ break;
+
+ case EmotionType.Surprise:
+ // Surprise/Shock - strong weights for game visibility
+ mappings.Add(new EmotionAnimationMapping("E_S_Shock1", 1.2f));
+ mappings.Add(new EmotionAnimationMapping("E_B_Shock1", 1.2f));
+ mappings.Add(new EmotionAnimationMapping("E_Y_Shock1", 1.2f));
+ break;
+
+ case EmotionType.Contempt:
+ // Contempt/Disdain - strong weights for game visibility
+ mappings.Add(new EmotionAnimationMapping("E_S_Disdain1", 1.2f));
+ mappings.Add(new EmotionAnimationMapping("E_S_Disdain2", 1.0f));
+ mappings.Add(new EmotionAnimationMapping("E_B_Disdain1", 1.2f));
+ mappings.Add(new EmotionAnimationMapping("E_B_Disdain2", 1.0f));
+ mappings.Add(new EmotionAnimationMapping("E_Y_Disdain1", 1.2f));
+ mappings.Add(new EmotionAnimationMapping("E_Y_Disdain2", 1.0f));
+ break;
+
+ case EmotionType.Determined:
+ // Determined/Stern - strong weights for game visibility
+ mappings.Add(new EmotionAnimationMapping("E_S_Stern1", 1.2f));
+ mappings.Add(new EmotionAnimationMapping("E_S_Stern2", 1.0f));
+ mappings.Add(new EmotionAnimationMapping("E_B_Stern1", 1.2f));
+ mappings.Add(new EmotionAnimationMapping("E_B_Stern2", 1.0f));
+ mappings.Add(new EmotionAnimationMapping("E_Y_Stern1", 1.2f));
+ mappings.Add(new EmotionAnimationMapping("E_Y_Stern2", 1.0f));
+ break;
+
+ case EmotionType.Worried:
+ // Worried/Concern - strong weights for game visibility
+ mappings.Add(new EmotionAnimationMapping("E_S_Concern1", 1.2f));
+ mappings.Add(new EmotionAnimationMapping("E_S_Concern2", 1.0f));
+ mappings.Add(new EmotionAnimationMapping("E_B_Concern1", 1.2f));
+ mappings.Add(new EmotionAnimationMapping("E_B_Concern2", 1.0f));
+ mappings.Add(new EmotionAnimationMapping("E_Y_Concern1", 1.2f));
+ mappings.Add(new EmotionAnimationMapping("E_Y_Concern2", 1.0f));
+ break;
+ }
+
+ return mappings;
+ }
+
+ ///
+ /// Adds an emotion mapping only if the animation exists in the available set
+ ///
+ private void AddIfAvailable(List mappings, HashSet available, string animName, float weight)
+ {
+ if (available.Contains(animName))
+ {
+ mappings.Add(new EmotionAnimationMapping(animName, weight));
+ }
+ }
+
+ ///
+ /// Mapping for emotion to animation
+ ///
+ private class EmotionAnimationMapping
+ {
+ public string AnimationName { get; }
+ public float Weight { get; }
+
+ public EmotionAnimationMapping(string name, float weight)
+ {
+ AnimationName = name;
+ Weight = weight;
+ }
+ }
+
+ private void AddAnimation(string name, List points)
+ {
+ // Initialize lists if null
+ if (_line.AnimationNames == null)
+ _line.AnimationNames = new List();
+ if (_line.NumKeys == null)
+ _line.NumKeys = new List();
+ if (_line.Points == null)
+ _line.Points = new List();
+
+ // Check if animation already exists
+ int existingIndex = -1;
+ for (int i = 0; i < _line.AnimationNames.Count; i++)
+ {
+ int nameIdx = _line.AnimationNames[i];
+ if (nameIdx >= 0 && nameIdx < _faceFX.Names.Count && _faceFX.Names[nameIdx] == name)
+ {
+ existingIndex = i;
+ break;
+ }
+ }
+
+ if (existingIndex >= 0)
+ {
+ // Update existing animation
+ int pointOffset = 0;
+ for (int i = 0; i < existingIndex; i++)
+ {
+ pointOffset += _line.NumKeys[i];
+ }
+
+ // Remove old points
+ int oldNumKeys = _line.NumKeys[existingIndex];
+ if (pointOffset >= 0 && oldNumKeys > 0 && pointOffset + oldNumKeys <= _line.Points.Count)
+ {
+ _line.Points.RemoveRange(pointOffset, oldNumKeys);
+ }
+
+ // Insert new points
+ if (pointOffset <= _line.Points.Count)
+ {
+ _line.Points.InsertRange(pointOffset, points);
+ }
+ else
+ {
+ _line.Points.AddRange(points);
+ }
+ _line.NumKeys[existingIndex] = points.Count;
+ }
+ else
+ {
+ // Add new animation
+ int nameIndex = _faceFX.Names.IndexOf(name);
+ if (nameIndex < 0)
+ {
+ nameIndex = _faceFX.Names.Count;
+ _faceFX.Names.Add(name);
+ }
+
+ _line.AnimationNames.Add(nameIndex);
+ _line.NumKeys.Add(points.Count);
+ _line.Points.AddRange(points);
+ }
+ }
+ }
+}
diff --git a/LegendaryExplorer/LegendaryExplorer/LegendaryExplorer/Tools/FaceFXEditor/AutoFaceFXGenerator/FxaXmlParser.cs b/LegendaryExplorer/LegendaryExplorer/LegendaryExplorer/Tools/FaceFXEditor/AutoFaceFXGenerator/FxaXmlParser.cs
new file mode 100644
index 0000000000..9b1320aad2
--- /dev/null
+++ b/LegendaryExplorer/LegendaryExplorer/LegendaryExplorer/Tools/FaceFXEditor/AutoFaceFXGenerator/FxaXmlParser.cs
@@ -0,0 +1,873 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Xml.Linq;
+
+namespace LegendaryExplorer.Tools.FaceFXEditor.AutoFaceFXGenerator
+{
+ ///
+ /// Parses FaceFX files from UDK FaceFX Studio
+ /// Supports: FXA (binary and XML), FXT (text phoneme data)
+ ///
+ public static class FxaXmlParser
+ {
+ // FaceFX binary file magic bytes
+ private static readonly byte[] FXA_MAGIC = { 0x46, 0x41, 0x43, 0x45 }; // "FACE"
+
+ ///
+ /// Parse an FXA file (binary or XML format from FaceFX Studio)
+ ///
+ public static FxaAnimationData ParseFxaFile(string filePath)
+ {
+ if (!File.Exists(filePath))
+ throw new FileNotFoundException("FXA file not found", filePath);
+
+ // First, check if it's a binary file by reading the first few bytes
+ byte[] header = new byte[4];
+ using (var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read))
+ {
+ fs.Read(header, 0, 4);
+ }
+
+ // Check for binary FXA format
+ if (IsBinaryFxa(header))
+ {
+ return ParseBinaryFxa(filePath);
+ }
+
+ // Otherwise try text-based parsing
+ var content = File.ReadAllText(filePath);
+
+ // Check if it's XML
+ if (content.TrimStart().StartsWith("<") || content.TrimStart().StartsWith("
+ /// Check if the file header indicates a binary FXA file
+ ///
+ private static bool IsBinaryFxa(byte[] header)
+ {
+ // Check for "FACE" magic or other common FaceFX binary markers
+ if (header.Length >= 4)
+ {
+ // "FACE" header
+ if (header[0] == 0x46 && header[1] == 0x41 && header[2] == 0x43 && header[3] == 0x45)
+ return true;
+
+ // Check for other binary indicators (non-printable characters in what should be text)
+ int nonPrintable = 0;
+ for (int i = 0; i < 4; i++)
+ {
+ if (header[i] < 32 && header[i] != 9 && header[i] != 10 && header[i] != 13)
+ nonPrintable++;
+ }
+ if (nonPrintable > 1)
+ return true;
+ }
+ return false;
+ }
+
+ ///
+ /// Parse a binary FXA file from FaceFX Studio
+ ///
+ private static FxaAnimationData ParseBinaryFxa(string filePath)
+ {
+ var result = new FxaAnimationData();
+
+ using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read);
+ using var reader = new BinaryReader(fs);
+
+ try
+ {
+ // Read header
+ byte[] magic = reader.ReadBytes(4);
+
+ // Try to identify the format version
+ // FaceFX binary format varies by version, but generally has:
+ // - Magic header
+ // - Version info
+ // - String table
+ // - Bone data
+ // - Animation data
+
+ // Read version (usually 4 bytes after magic)
+ int version = 0;
+ if (fs.Length > 8)
+ {
+ version = reader.ReadInt32();
+ }
+
+ // Skip to find animation data
+ // This is a simplified parser - full FXA binary parsing would require
+ // reverse engineering the complete format
+
+ // Try to find string markers that indicate animation names
+ fs.Position = 0;
+ byte[] fileData = reader.ReadBytes((int)fs.Length);
+
+ // Search for animation name patterns (m_*) in the binary
+ var animationNames = FindAnimationNamesInBinary(fileData);
+
+ // Search for float data that could be animation curves
+ var curveData = FindCurveDataInBinary(fileData, animationNames);
+
+ foreach (var kvp in curveData)
+ {
+ if (kvp.Value.Count > 0)
+ {
+ result.Animations[kvp.Key] = new FxaAnimation
+ {
+ Name = kvp.Key,
+ };
+ result.Animations[kvp.Key].Keys.AddRange(kvp.Value);
+ }
+ }
+
+ if (result.Animations.Count == 0)
+ {
+ throw new InvalidDataException(
+ "Could not extract animation data from binary FXA file.\n" +
+ "The binary format may be unsupported. Try exporting as XML from FaceFX Studio.");
+ }
+ }
+ catch (EndOfStreamException)
+ {
+ throw new InvalidDataException("Unexpected end of file while parsing binary FXA.");
+ }
+
+ return result;
+ }
+
+ ///
+ /// Search for animation names (m_*) in binary data
+ ///
+ private static List FindAnimationNamesInBinary(byte[] data)
+ {
+ var names = new List();
+ var knownNames = new[]
+ {
+ "m_Jaw+", "m_Jaw-", "m_Open", "m_M", "m_EE", "m_N", "m_G",
+ "m_OW", "m_OH", "m_Flap", "m_FV", "m_TH", "m_L", "m_ZZ", "m_EH"
+ };
+
+ string dataStr = Encoding.ASCII.GetString(data);
+
+ foreach (var name in knownNames)
+ {
+ if (dataStr.Contains(name))
+ {
+ names.Add(name);
+ }
+ }
+
+ // Also search for generic m_ patterns
+ var regex = new Regex(@"m_[A-Za-z0-9+\-]+");
+ var matches = regex.Matches(dataStr);
+ foreach (Match match in matches)
+ {
+ if (!names.Contains(match.Value))
+ {
+ names.Add(match.Value);
+ }
+ }
+
+ return names;
+ }
+
+ ///
+ /// Attempt to find curve data associated with animation names in binary
+ ///
+ private static Dictionary> FindCurveDataInBinary(byte[] data, List animNames)
+ {
+ var result = new Dictionary>();
+
+ foreach (var name in animNames)
+ {
+ result[name] = new List();
+ }
+
+ // Search for float arrays that could be curve data
+ // Floats in binary are typically 4 bytes each
+ // We look for sequences that could be time/value pairs
+
+ string dataStr = Encoding.ASCII.GetString(data);
+
+ foreach (var animName in animNames)
+ {
+ int nameIndex = dataStr.IndexOf(animName);
+ if (nameIndex < 0) continue;
+
+ // Look for float data after the name (within next ~1KB)
+ int searchStart = nameIndex + animName.Length;
+ int searchEnd = Math.Min(searchStart + 1024, data.Length - 8);
+
+ var floats = new List();
+ for (int i = searchStart; i < searchEnd - 4; i += 4)
+ {
+ try
+ {
+ float f = BitConverter.ToSingle(data, i);
+ // Check if it's a reasonable float value (not NaN, not huge)
+ if (!float.IsNaN(f) && !float.IsInfinity(f) && Math.Abs(f) < 1000)
+ {
+ floats.Add(f);
+ }
+ }
+ catch { }
+ }
+
+ // Try to interpret floats as time/value pairs
+ if (floats.Count >= 4)
+ {
+ for (int i = 0; i < floats.Count - 1; i += 2)
+ {
+ float time = floats[i];
+ float value = floats[i + 1];
+
+ // Sanity check: time should be positive and value should be in reasonable range
+ if (time >= 0 && time < 100 && value >= -10 && value <= 10)
+ {
+ result[animName].Add(new FxaKey
+ {
+ Time = time,
+ Value = value,
+ InTangent = 0f,
+ OutTangent = 0f
+ });
+ }
+ }
+ }
+ }
+
+ return result;
+ }
+
+ ///
+ /// Parse an FXT file (FaceFX text phoneme timing data)
+ ///
+ public static FxaAnimationData ParseFxtFile(string filePath)
+ {
+ if (!File.Exists(filePath))
+ throw new FileNotFoundException("FXT file not found", filePath);
+
+ var content = File.ReadAllText(filePath);
+ return ParseFxtContent(content);
+ }
+
+ ///
+ /// Parse FXT content (phoneme timing data)
+ /// FXT format typically contains lines like:
+ /// phoneme startTime endTime
+ /// or word-based timing
+ ///
+ public static FxaAnimationData ParseFxtContent(string content)
+ {
+ var result = new FxaAnimationData();
+ var lines = content.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
+
+ // Collect all phoneme events with timing
+ var phonemeEvents = new List<(string phoneme, float startTime, float endTime)>();
+
+ foreach (var line in lines)
+ {
+ var trimmed = line.Trim();
+ if (string.IsNullOrEmpty(trimmed) || trimmed.StartsWith("#") || trimmed.StartsWith("//"))
+ continue;
+
+ var parts = trimmed.Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries);
+
+ // Format: phoneme startTime endTime
+ // or: startTime endTime phoneme
+ if (parts.Length >= 3)
+ {
+ string phoneme;
+ float startTime, endTime;
+
+ // Try both formats
+ if (TryParseFloat(parts[0], out startTime) && TryParseFloat(parts[1], out endTime))
+ {
+ // Format: startTime endTime phoneme
+ phoneme = parts[2];
+ }
+ else if (TryParseFloat(parts[1], out startTime) && TryParseFloat(parts[2], out endTime))
+ {
+ // Format: phoneme startTime endTime
+ phoneme = parts[0];
+ }
+ else
+ {
+ continue;
+ }
+
+ phonemeEvents.Add((phoneme.ToUpper(), startTime, endTime));
+ }
+ }
+
+ // Convert phoneme events to animation curves using the phoneme mapping
+ if (phonemeEvents.Count > 0)
+ {
+ result.PhonemeEvents = phonemeEvents;
+ ConvertPhonemesToAnimations(phonemeEvents, result);
+ }
+
+ return result;
+ }
+
+ ///
+ /// Convert phoneme timing events into animation curves
+ ///
+ private static void ConvertPhonemesToAnimations(
+ List<(string phoneme, float startTime, float endTime)> phonemeEvents,
+ FxaAnimationData result)
+ {
+ // Create animation curves for each viseme
+ var visemeCurves = new Dictionary>();
+
+ foreach (var (phoneme, startTime, endTime) in phonemeEvents)
+ {
+ // Look up the phoneme mapping
+ if (PhonemeToVisemeMap.PhonemeMap.TryGetValue(phoneme, out var mappings))
+ {
+ float centerTime = (startTime + endTime) / 2f;
+ float duration = endTime - startTime;
+
+ foreach (var mapping in mappings)
+ {
+ if (!visemeCurves.ContainsKey(mapping.VisemeName))
+ {
+ visemeCurves[mapping.VisemeName] = new List();
+ }
+
+ // Add attack key (ramp up)
+ visemeCurves[mapping.VisemeName].Add(new FxaKey
+ {
+ Time = startTime,
+ Value = 0f,
+ InTangent = 0f,
+ OutTangent = 0f
+ });
+
+ // Add peak key
+ visemeCurves[mapping.VisemeName].Add(new FxaKey
+ {
+ Time = centerTime,
+ Value = mapping.Weight,
+ InTangent = 0f,
+ OutTangent = 0f
+ });
+
+ // Add release key (ramp down)
+ visemeCurves[mapping.VisemeName].Add(new FxaKey
+ {
+ Time = endTime,
+ Value = 0f,
+ InTangent = 0f,
+ OutTangent = 0f
+ });
+ }
+ }
+ }
+
+ // Convert to FxaAnimation objects
+ foreach (var kvp in visemeCurves)
+ {
+ var anim = new FxaAnimation { Name = kvp.Key };
+
+ // Sort keys by time and merge nearby ones
+ var sortedKeys = kvp.Value.OrderBy(k => k.Time).ToList();
+ var mergedKeys = MergeNearbyKeys(sortedKeys, 0.02f);
+
+ anim.Keys.AddRange(mergedKeys);
+ result.Animations[kvp.Key] = anim;
+ }
+ }
+
+ ///
+ /// Merge keys that are too close together
+ ///
+ private static List MergeNearbyKeys(List keys, float threshold)
+ {
+ if (keys.Count < 2)
+ return keys;
+
+ var result = new List { keys[0] };
+
+ for (int i = 1; i < keys.Count; i++)
+ {
+ var last = result[^1];
+ var current = keys[i];
+
+ if (current.Time - last.Time < threshold)
+ {
+ // Merge by taking the higher value
+ if (current.Value > last.Value)
+ {
+ result[^1] = current;
+ }
+ }
+ else
+ {
+ result.Add(current);
+ }
+ }
+
+ return result;
+ }
+
+ ///
+ /// Parse FXA XML content
+ ///
+ public static FxaAnimationData ParseFxaXml(string xmlContent)
+ {
+ var result = new FxaAnimationData();
+
+ try
+ {
+ var doc = XDocument.Parse(xmlContent);
+ var root = doc.Root;
+
+ if (root == null)
+ throw new InvalidDataException("Invalid FXA XML: no root element");
+
+ // Find animation elements - try multiple possible structures
+ ParseAnimationsFromXml(root, result);
+
+ // Parse the phoneme mapping if present
+ var mapping = root.Descendants("mapping").FirstOrDefault();
+ if (mapping != null)
+ {
+ ParsePhonemeMapping(mapping, result);
+ }
+
+ // Parse face graph for bone information
+ var faceGraph = root.Descendants("face_graph").FirstOrDefault();
+ if (faceGraph != null)
+ {
+ ParseFaceGraph(faceGraph, result);
+ }
+ }
+ catch (System.Xml.XmlException ex)
+ {
+ throw new InvalidDataException($"Invalid XML format: {ex.Message}", ex);
+ }
+
+ return result;
+ }
+
+ ///
+ /// Try to parse text-based FXA format (non-XML)
+ ///
+ private static FxaAnimationData ParseFxaText(string content)
+ {
+ var result = new FxaAnimationData();
+
+ // Try to find animation data patterns in text format
+ // This handles various text export formats from FaceFX
+
+ // Pattern: "anim animName" followed by curve data
+ var animPattern = new Regex(@"anim\s+[""']?(\w+)[""']?\s*\{([^}]+)\}", RegexOptions.IgnoreCase);
+ var matches = animPattern.Matches(content);
+
+ foreach (Match match in matches)
+ {
+ var animName = match.Groups[1].Value;
+ var animData = match.Groups[2].Value;
+
+ var anim = new FxaAnimation { Name = animName };
+ ParseTextCurveData(animData, anim);
+
+ if (anim.Keys.Count > 0)
+ {
+ result.Animations[animName] = anim;
+ }
+ }
+
+ // If no structured format found, try line-by-line parsing
+ if (result.Animations.Count == 0)
+ {
+ ParseUnstructuredText(content, result);
+ }
+
+ return result;
+ }
+
+ private static void ParseTextCurveData(string data, FxaAnimation anim)
+ {
+ // Look for key data: time value [tangentIn tangentOut]
+ var keyPattern = new Regex(@"([\d.-]+)\s+([\d.-]+)(?:\s+([\d.-]+)\s+([\d.-]+))?");
+ var matches = keyPattern.Matches(data);
+
+ foreach (Match match in matches)
+ {
+ if (TryParseFloat(match.Groups[1].Value, out float time) &&
+ TryParseFloat(match.Groups[2].Value, out float value))
+ {
+ float inTangent = 0f, outTangent = 0f;
+ if (match.Groups[3].Success && match.Groups[4].Success)
+ {
+ TryParseFloat(match.Groups[3].Value, out inTangent);
+ TryParseFloat(match.Groups[4].Value, out outTangent);
+ }
+
+ anim.Keys.Add(new FxaKey
+ {
+ Time = time,
+ Value = value,
+ InTangent = inTangent,
+ OutTangent = outTangent
+ });
+ }
+ }
+ }
+
+ private static void ParseUnstructuredText(string content, FxaAnimationData result)
+ {
+ // Try to identify animation names and their data from unstructured text
+ var lines = content.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
+
+ string currentAnimName = null;
+ FxaAnimation currentAnim = null;
+
+ foreach (var line in lines)
+ {
+ var trimmed = line.Trim();
+
+ // Check if this line starts a new animation
+ if (trimmed.StartsWith("m_", StringComparison.OrdinalIgnoreCase))
+ {
+ // Could be animation name
+ var parts = trimmed.Split(new[] { ' ', '\t', ':' }, StringSplitOptions.RemoveEmptyEntries);
+ if (parts.Length >= 1)
+ {
+ currentAnimName = parts[0];
+ currentAnim = new FxaAnimation { Name = currentAnimName };
+ result.Animations[currentAnimName] = currentAnim;
+ }
+ }
+ else if (currentAnim != null)
+ {
+ // Try to parse as key data
+ var parts = trimmed.Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries);
+ if (parts.Length >= 2 &&
+ TryParseFloat(parts[0], out float time) &&
+ TryParseFloat(parts[1], out float value))
+ {
+ currentAnim.Keys.Add(new FxaKey
+ {
+ Time = time,
+ Value = value,
+ InTangent = 0f,
+ OutTangent = 0f
+ });
+ }
+ }
+ }
+ }
+
+ private static void ParseAnimationsFromXml(XElement root, FxaAnimationData result)
+ {
+ // Try various XML structures that FaceFX might export
+
+ // Structure 1: anim_group > anim
+ var animGroups = root.Descendants("anim_group");
+ foreach (var group in animGroups)
+ {
+ foreach (var anim in group.Elements("anim"))
+ {
+ ParseAnimation(anim, result);
+ }
+ }
+
+ // Structure 2: Direct anim elements
+ var animations = root.Descendants("anim").ToList();
+ foreach (var anim in animations)
+ {
+ ParseAnimation(anim, result);
+ }
+
+ // Structure 3: animation elements
+ var animationElements = root.Descendants("animation").ToList();
+ foreach (var anim in animationElements)
+ {
+ ParseAnimationElement(anim, result);
+ }
+
+ // Structure 4: curve elements
+ var curves = root.Descendants("curve").ToList();
+ foreach (var curve in curves)
+ {
+ ParseCurveElement(curve, result);
+ }
+ }
+
+ private static void ParseAnimation(XElement anim, FxaAnimationData result)
+ {
+ var name = anim.Attribute("name")?.Value;
+ if (string.IsNullOrEmpty(name))
+ return;
+
+ // Skip if already parsed
+ if (result.Animations.ContainsKey(name))
+ return;
+
+ var animData = new FxaAnimation { Name = name };
+
+ // Try various key formats
+ ParseKeysFromElement(anim, animData);
+
+ if (animData.Keys.Count > 0)
+ {
+ result.Animations[name] = animData;
+ }
+ }
+
+ private static void ParseAnimationElement(XElement anim, FxaAnimationData result)
+ {
+ var name = anim.Attribute("name")?.Value ?? anim.Element("name")?.Value;
+ if (string.IsNullOrEmpty(name))
+ return;
+
+ if (result.Animations.ContainsKey(name))
+ return;
+
+ var animData = new FxaAnimation { Name = name };
+ ParseKeysFromElement(anim, animData);
+
+ if (animData.Keys.Count > 0)
+ {
+ result.Animations[name] = animData;
+ }
+ }
+
+ private static void ParseCurveElement(XElement curve, FxaAnimationData result)
+ {
+ var name = curve.Attribute("name")?.Value ?? curve.Attribute("target")?.Value;
+ if (string.IsNullOrEmpty(name))
+ return;
+
+ if (result.Animations.ContainsKey(name))
+ return;
+
+ var animData = new FxaAnimation { Name = name };
+ ParseKeysFromElement(curve, animData);
+
+ if (animData.Keys.Count > 0)
+ {
+ result.Animations[name] = animData;
+ }
+ }
+
+ private static void ParseKeysFromElement(XElement element, FxaAnimation animData)
+ {
+ // Try curve_keys element
+ var curveKeys = element.Element("curve_keys");
+ if (curveKeys != null)
+ {
+ ParseCurveKeysText(curveKeys.Value, animData);
+ }
+
+ // Try keys element
+ var keys = element.Element("keys");
+ if (keys != null)
+ {
+ ParseCurveKeysText(keys.Value, animData);
+ }
+
+ // Try keyframes element
+ var keyframes = element.Element("keyframes");
+ if (keyframes != null)
+ {
+ ParseCurveKeysText(keyframes.Value, animData);
+ }
+
+ // Try individual key elements
+ var keyElements = element.Elements("key").ToList();
+ foreach (var key in keyElements)
+ {
+ var timeAttr = key.Attribute("time")?.Value ?? key.Attribute("t")?.Value;
+ var valueAttr = key.Attribute("value")?.Value ?? key.Attribute("v")?.Value;
+
+ if (TryParseFloat(timeAttr, out float time) && TryParseFloat(valueAttr, out float value))
+ {
+ TryParseFloat(key.Attribute("in")?.Value, out float inTan);
+ TryParseFloat(key.Attribute("out")?.Value, out float outTan);
+
+ animData.Keys.Add(new FxaKey
+ {
+ Time = time,
+ Value = value,
+ InTangent = inTan,
+ OutTangent = outTan
+ });
+ }
+ }
+
+ // Try data attribute or element content
+ var dataAttr = element.Attribute("data")?.Value;
+ if (!string.IsNullOrEmpty(dataAttr))
+ {
+ ParseCurveKeysText(dataAttr, animData);
+ }
+
+ // Try element text content directly
+ if (animData.Keys.Count == 0 && !element.HasElements)
+ {
+ ParseCurveKeysText(element.Value, animData);
+ }
+ }
+
+ private static void ParseCurveKeysText(string keysText, FxaAnimation animData)
+ {
+ if (string.IsNullOrWhiteSpace(keysText))
+ return;
+
+ var values = keysText.Split(new[] { ' ', '\t', '\n', '\r', ',' }, StringSplitOptions.RemoveEmptyEntries);
+
+ if (values.Length < 2)
+ return;
+
+ // Determine format: 2 values (time, value) or 4 values (time, value, inTan, outTan)
+ if (values.Length >= 4 && values.Length % 4 == 0)
+ {
+ for (int i = 0; i < values.Length; i += 4)
+ {
+ if (TryParseFloat(values[i], out float time) &&
+ TryParseFloat(values[i + 1], out float value))
+ {
+ TryParseFloat(values[i + 2], out float inTangent);
+ TryParseFloat(values[i + 3], out float outTangent);
+
+ animData.Keys.Add(new FxaKey
+ {
+ Time = time,
+ Value = value,
+ InTangent = inTangent,
+ OutTangent = outTangent
+ });
+ }
+ }
+ }
+ else if (values.Length >= 2 && values.Length % 2 == 0)
+ {
+ for (int i = 0; i < values.Length; i += 2)
+ {
+ if (TryParseFloat(values[i], out float time) &&
+ TryParseFloat(values[i + 1], out float value))
+ {
+ animData.Keys.Add(new FxaKey
+ {
+ Time = time,
+ Value = value,
+ InTangent = 0f,
+ OutTangent = 0f
+ });
+ }
+ }
+ }
+ }
+
+ private static void ParsePhonemeMapping(XElement mapping, FxaAnimationData result)
+ {
+ var entries = mapping.Elements("entry");
+ foreach (var entry in entries)
+ {
+ var phoneme = entry.Attribute("phoneme")?.Value;
+ var target = entry.Attribute("target")?.Value;
+ var amountStr = entry.Attribute("amount")?.Value;
+
+ if (!string.IsNullOrEmpty(phoneme) && !string.IsNullOrEmpty(target) &&
+ TryParseFloat(amountStr, out float amount))
+ {
+ if (!result.PhonemeMapping.ContainsKey(phoneme))
+ {
+ result.PhonemeMapping[phoneme] = new List();
+ }
+ result.PhonemeMapping[phoneme].Add(new FxaPhonemeTarget
+ {
+ Target = target,
+ Amount = amount
+ });
+ }
+ }
+ }
+
+ private static void ParseFaceGraph(XElement faceGraph, FxaAnimationData result)
+ {
+ var bones = faceGraph.Descendants("bone");
+ foreach (var bone in bones)
+ {
+ var name = bone.Attribute("name")?.Value;
+ if (!string.IsNullOrEmpty(name))
+ {
+ result.BoneNames.Add(name);
+ }
+ }
+ }
+
+ private static bool TryParseFloat(string value, out float result)
+ {
+ result = 0f;
+ if (string.IsNullOrEmpty(value))
+ return false;
+ return float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out result);
+ }
+ }
+
+ ///
+ /// Container for all animation data from FXA/FXT files
+ ///
+ public class FxaAnimationData
+ {
+ public Dictionary Animations { get; } = new();
+ public Dictionary> PhonemeMapping { get; } = new();
+ public List BoneNames { get; } = new();
+ public List<(string phoneme, float startTime, float endTime)> PhonemeEvents { get; set; } = new();
+ }
+
+ ///
+ /// A single animation curve from FXA
+ ///
+ public class FxaAnimation
+ {
+ public string Name { get; set; }
+ public List Keys { get; } = new();
+ }
+
+ ///
+ /// A keyframe in an animation curve
+ ///
+ public class FxaKey
+ {
+ public float Time { get; set; }
+ public float Value { get; set; }
+ public float InTangent { get; set; }
+ public float OutTangent { get; set; }
+ }
+
+ ///
+ /// A phoneme to viseme target mapping
+ ///
+ public class FxaPhonemeTarget
+ {
+ public string Target { get; set; }
+ public float Amount { get; set; }
+ }
+}
diff --git a/LegendaryExplorer/LegendaryExplorer/LegendaryExplorer/Tools/FaceFXEditor/AutoFaceFXGenerator/PhonemeToVisemeMap.cs b/LegendaryExplorer/LegendaryExplorer/LegendaryExplorer/Tools/FaceFXEditor/AutoFaceFXGenerator/PhonemeToVisemeMap.cs
new file mode 100644
index 0000000000..9cb5819174
--- /dev/null
+++ b/LegendaryExplorer/LegendaryExplorer/LegendaryExplorer/Tools/FaceFXEditor/AutoFaceFXGenerator/PhonemeToVisemeMap.cs
@@ -0,0 +1,2406 @@
+using System.Collections.Generic;
+
+namespace LegendaryExplorer.Tools.FaceFXEditor.AutoFaceFXGenerator
+{
+ ///
+ /// Supported species for FaceFX generation
+ ///
+ public enum FaceFXSpecies
+ {
+ HumanFemale,
+ HumanMale,
+ HumanChild,
+ Asari,
+ Krogan,
+ Drell,
+ Turian,
+ Salarian,
+ Quarian,
+ Geth,
+ Elcor,
+ Hanar,
+ Volus,
+ Batarian,
+ Vorcha,
+ Prothean,
+ Yahg
+ }
+
+ ///
+ /// Maps phonemes to visemes using UDK FaceFX reference data.
+ /// Each phoneme maps to multiple visemes with specific weights.
+ ///
+ public static class PhonemeToVisemeMap
+ {
+ ///
+ /// Gets the phoneme map for the specified species.
+ /// Note: Some species (Elcor, Hanar, Volus, Batarian, Vorcha, Yahg) have
+ /// FaceFX data that uses bone names as phoneme identifiers instead of standard phonemes.
+ /// For these species, we use Drell phoneme map which has standard phonemes with similar viseme names.
+ /// Prothean uses m_* style visemes like Human Male, so it uses HumanMalePhonemeMap.
+ ///
+ public static Dictionary GetPhonemeMap(FaceFXSpecies species)
+ {
+ return species switch
+ {
+ FaceFXSpecies.HumanMale => HumanMalePhonemeMap,
+ FaceFXSpecies.HumanChild => HumanChildPhonemeMap,
+ FaceFXSpecies.Asari => AsariPhonemeMap,
+ FaceFXSpecies.Krogan => KroganPhonemeMap,
+ FaceFXSpecies.Drell => DrellPhonemeMap,
+ FaceFXSpecies.Turian => TurianPhonemeMap,
+ FaceFXSpecies.Salarian => SalarianPhonemeMap,
+ FaceFXSpecies.Quarian => QuarianPhonemeMap,
+ FaceFXSpecies.Geth => GethPhonemeMap,
+ // These species have bone-based phoneme maps that don't match standard phonemes.
+ // Use Drell as fallback since it has standard phonemes with similar viseme names (jawOpen, smileRight, etc.)
+ FaceFXSpecies.Elcor => DrellPhonemeMap,
+ FaceFXSpecies.Hanar => DrellPhonemeMap,
+ FaceFXSpecies.Volus => DrellPhonemeMap,
+ FaceFXSpecies.Batarian => DrellPhonemeMap,
+ FaceFXSpecies.Vorcha => DrellPhonemeMap,
+ // Prothean uses m_* style visemes like Human Male (m_Open, m_Jaw+, m_OH, m_EE, etc.)
+ FaceFXSpecies.Prothean => HumanMalePhonemeMap,
+ // Yahg has unique visemes - use Drell as closest approximation
+ FaceFXSpecies.Yahg => DrellPhonemeMap,
+ _ => HumanFemalePhonemeMap
+ };
+ }
+
+ ///
+ /// Gets the viseme animation names for the specified species
+ ///
+ public static string[] GetVisemes(FaceFXSpecies species)
+ {
+ return species switch
+ {
+ FaceFXSpecies.HumanMale => HumanMaleVisemes,
+ FaceFXSpecies.HumanChild => HumanChildVisemes,
+ FaceFXSpecies.Asari => AsariVisemes,
+ FaceFXSpecies.Krogan => KroganVisemes,
+ FaceFXSpecies.Drell => DrellVisemes,
+ FaceFXSpecies.Turian => TurianVisemes,
+ FaceFXSpecies.Salarian => SalarianVisemes,
+ FaceFXSpecies.Quarian => QuarianVisemes,
+ FaceFXSpecies.Geth => GethVisemes,
+ FaceFXSpecies.Elcor => ElcorVisemes,
+ FaceFXSpecies.Hanar => HanarVisemes,
+ FaceFXSpecies.Volus => VolusVisemes,
+ FaceFXSpecies.Batarian => BatarianVisemes,
+ FaceFXSpecies.Vorcha => VorchaVisemes,
+ FaceFXSpecies.Prothean => ProtheanVisemes,
+ FaceFXSpecies.Yahg => YahgVisemes,
+ _ => HumanFemaleVisemes
+ };
+ }
+
+ ///
+ /// Human Female phoneme to viseme mappings - EXACT values from Unreal FaceFX.
+ /// Each phoneme can trigger multiple visemes with specific weights.
+ ///
+ public static readonly Dictionary HumanFemalePhonemeMap = new()
+ {
+ // Silence
+ { "SIL", new[] { new VisemeMapping("m_Jaw+", 0.107f), new VisemeMapping("m_Open", 0.162f) } },
+
+ // Bilabial stops
+ { "P", new[] { new VisemeMapping("m_M", 0.730f), new VisemeMapping("m_Jaw-", 0.112f) } },
+ { "B", new[] { new VisemeMapping("m_M", 0.765f), new VisemeMapping("m_Jaw-", 0.760f) } },
+ { "M", new[] { new VisemeMapping("m_M", 1.000f), new VisemeMapping("m_Jaw-", 0.186f) } },
+
+ // Alveolar stops
+ { "T", new[] { new VisemeMapping("m_EE", 0.407f), new VisemeMapping("m_N", 1.000f), new VisemeMapping("m_G", 0.128f) } },
+ { "D", new[] { new VisemeMapping("m_Jaw-", 0.055f), new VisemeMapping("m_N", 1.000f) } },
+
+ // Velar stops
+ { "K", new[] { new VisemeMapping("m_Jaw+", 0.250f), new VisemeMapping("m_G", 0.364f) } },
+ { "G", new[] { new VisemeMapping("m_Jaw+", 0.090f), new VisemeMapping("m_G", 0.657f) } },
+
+ // Nasals
+ { "N", new[] { new VisemeMapping("m_EE", 0.496f), new VisemeMapping("m_OW", 0.496f), new VisemeMapping("m_Jaw-", 0.110f), new VisemeMapping("m_N", 1.000f) } },
+ { "NG", new[] { new VisemeMapping("m_EE", 0.499f), new VisemeMapping("m_Jaw+", 0.123f), new VisemeMapping("m_M", 0.509f), new VisemeMapping("m_N", 1.000f) } },
+
+ // Fricatives
+ { "F", new[] { new VisemeMapping("m_Jaw+", 0.129f), new VisemeMapping("m_FV", 0.765f) } },
+ { "V", new[] { new VisemeMapping("m_Jaw+", 0.151f), new VisemeMapping("m_FV", 1.000f) } },
+ { "TH", new[] { new VisemeMapping("m_Jaw+", 0.157f), new VisemeMapping("m_TH", 0.724f) } },
+ { "DH", new[] { new VisemeMapping("m_Jaw+", 0.216f), new VisemeMapping("m_TH", 0.932f) } },
+ { "S", new[] { new VisemeMapping("m_EE", 0.262f), new VisemeMapping("m_Jaw+", 0.090f), new VisemeMapping("m_OW", 0.614f), new VisemeMapping("m_M", 0.472f) } },
+ { "Z", new[] { new VisemeMapping("m_EE", 0.267f), new VisemeMapping("m_Jaw+", 0.018f), new VisemeMapping("m_OW", 0.572f), new VisemeMapping("m_M", 0.648f) } },
+ { "SH", new[] { new VisemeMapping("m_Jaw+", 0.090f), new VisemeMapping("m_OW", 0.856f) } },
+ { "ZH", new[] { new VisemeMapping("m_Jaw+", 0.090f), new VisemeMapping("m_OW", 0.533f) } },
+ { "H", new[] { new VisemeMapping("m_EE", 0.353f), new VisemeMapping("m_Jaw+", 0.164f) } },
+
+ // Approximants
+ { "R", new[] { new VisemeMapping("m_Jaw+", 0.100f), new VisemeMapping("m_OH", 0.570f) } },
+ { "L", new[] { new VisemeMapping("m_Jaw+", 0.151f), new VisemeMapping("m_L", 1.000f) } },
+ { "W", new[] { new VisemeMapping("m_Jaw+", 0.136f), new VisemeMapping("m_OH", 0.709f) } },
+ { "Y", new[] { new VisemeMapping("m_EE", 0.223f), new VisemeMapping("m_Jaw+", 0.149f) } },
+
+ // Affricates
+ { "CH", new[] { new VisemeMapping("m_Jaw+", 0.088f), new VisemeMapping("m_Open", 1.000f) } },
+ { "JH", new[] { new VisemeMapping("m_Jaw+", 0.125f), new VisemeMapping("m_OH", 0.459f) } },
+
+ // Flap
+ { "FLAP", new[] { new VisemeMapping("m_Flap", 1.000f), new VisemeMapping("m_Jaw+", 0.220f) } },
+
+ // Special
+ { "TS", new[] { new VisemeMapping("m_Jaw-", 0.066f), new VisemeMapping("m_ZZ", 1.000f) } },
+
+ // Front vowels
+ { "IY", new[] { new VisemeMapping("m_EE", 0.318f), new VisemeMapping("m_Jaw+", 0.196f), new VisemeMapping("m_ZZ", 0.823f) } },
+ { "IH", new[] { new VisemeMapping("m_EE", 0.251f), new VisemeMapping("m_Jaw+", 0.253f), new VisemeMapping("m_OW", 0.336f) } },
+ { "EH", new[] { new VisemeMapping("m_Jaw+", 0.248f), new VisemeMapping("m_EH", 0.436f) } },
+ { "EY", new[] { new VisemeMapping("m_Jaw+", 0.435f), new VisemeMapping("m_Open", 0.305f) } },
+ { "AE", new[] { new VisemeMapping("m_Jaw+", 0.424f), new VisemeMapping("m_EH", 0.757f) } },
+
+ // Central vowels
+ { "AH", new[] { new VisemeMapping("m_Jaw+", 0.429f), new VisemeMapping("m_EH", 0.280f), new VisemeMapping("m_OH", 0.086f) } },
+ { "AX", new[] { new VisemeMapping("m_Jaw+", 0.454f), new VisemeMapping("m_Open", 0.442f) } },
+ { "ER", new[] { new VisemeMapping("m_Jaw+", 0.322f), new VisemeMapping("m_Open", 0.548f) } },
+
+ // Back vowels
+ { "UW", new[] { new VisemeMapping("m_Jaw+", 0.153f), new VisemeMapping("m_OH", 0.863f) } },
+ { "UH", new[] { new VisemeMapping("m_Jaw+", 0.264f), new VisemeMapping("m_OH", 0.659f) } },
+ { "OW", new[] { new VisemeMapping("m_Jaw+", 0.198f), new VisemeMapping("m_OH", 0.570f) } },
+ { "AA", new[] { new VisemeMapping("m_Jaw+", 0.459f), new VisemeMapping("m_Open", 0.403f), new VisemeMapping("m_OW", 0.403f) } },
+ { "AO", new[] { new VisemeMapping("m_Jaw+", 0.405f), new VisemeMapping("m_OH", 0.460f) } },
+
+ // Diphthongs
+ { "AY", new[] { new VisemeMapping("m_Jaw+", 0.313f) } },
+ { "AW", new[] { new VisemeMapping("m_Jaw+", 0.574f), new VisemeMapping("m_Open", 0.683f) } },
+ { "OY", new[] { new VisemeMapping("m_Jaw+", 0.261f), new VisemeMapping("m_OH", 0.570f) } },
+ };
+
+ ///
+ /// Legacy alias for backward compatibility
+ ///
+ public static readonly Dictionary PhonemeMap = HumanFemalePhonemeMap;
+
+ ///
+ /// Krogan phoneme to viseme mappings - from KRO_HED_PROBase_MDL_FaceFX data.
+ ///
+ public static readonly Dictionary KroganPhonemeMap = new()
+ {
+ // Silence
+ { "SIL", new[] { new VisemeMapping("jawOpen", 0.073224f) } },
+
+ // Bilabial stops
+ { "P", new[] { new VisemeMapping("jawOpen", 0.092563f), new VisemeMapping("smileRight", 0.068354f), new VisemeMapping("smileLeft", 0.070886f), new VisemeMapping("sneerRight", 0.144304f), new VisemeMapping("sneerLeft", 0.141210f), new VisemeMapping("lowerLipCurlin", 0.266426f), new VisemeMapping("upperLipCurlin", 0.091621f), new VisemeMapping("jawBack", 0.250599f) } },
+ { "B", new[] { new VisemeMapping("jawOpen", 0.113120f), new VisemeMapping("smileRight", 0.147117f), new VisemeMapping("smileLeft", 0.149648f), new VisemeMapping("sneerRight", 0.200000f), new VisemeMapping("sneerLeft", 0.200000f), new VisemeMapping("lowerLipCurlin", 0.230000f), new VisemeMapping("upperLipCurlin", 0.120000f), new VisemeMapping("jawBack", 0.050000f) } },
+ { "M", new[] { new VisemeMapping("jawOpen", 0.109595f), new VisemeMapping("smileRight", 0.372152f), new VisemeMapping("smileLeft", 0.326864f), new VisemeMapping("frownRight", 0.363713f), new VisemeMapping("frownLeft", 0.354993f), new VisemeMapping("lowerLipCurlin", 0.357989f), new VisemeMapping("upperLipCurlin", 0.188784f), new VisemeMapping("jawBack", 0.404822f) } },
+
+ // Alveolar stops
+ { "T", new[] { new VisemeMapping("jawOpen", 0.146185f), new VisemeMapping("smileRight", 0.150000f), new VisemeMapping("smileLeft", 0.150000f), new VisemeMapping("upperLipCurlOut", 0.151899f), new VisemeMapping("lowerLipCurlOut", 0.094635f), new VisemeMapping("sneerRight", 0.085232f), new VisemeMapping("sneerLeft", 0.084951f), new VisemeMapping("tongueUP", 1.000000f) } },
+ { "D", new[] { new VisemeMapping("jawOpen", 0.111023f), new VisemeMapping("smileRight", 0.200959f), new VisemeMapping("smileLeft", 0.205087f), new VisemeMapping("upperLipCurlOut", 0.188065f), new VisemeMapping("pucker", 0.179024f), new VisemeMapping("O_mouth", 0.088608f), new VisemeMapping("tongueUP", 0.570000f), new VisemeMapping("jawBack", 0.174522f) } },
+
+ // Velar stops
+ { "K", new[] { new VisemeMapping("jawOpen", 0.130362f), new VisemeMapping("smileRight", 0.353202f), new VisemeMapping("smileLeft", 0.354071f), new VisemeMapping("upperLipCurlOut", 0.145871f), new VisemeMapping("lowerLipCurlOut", 0.133816f), new VisemeMapping("sneerRight", 0.136811f), new VisemeMapping("sneerLeft", 0.136811f), new VisemeMapping("pucker", 0.148850f), new VisemeMapping("tongueUP", 0.710000f), new VisemeMapping("jawBack", 0.128102f) } },
+ { "G", new[] { new VisemeMapping("jawOpen", 0.133878f), new VisemeMapping("smileRight", 0.300000f), new VisemeMapping("smileLeft", 0.300000f), new VisemeMapping("upperLipCurlOut", 0.154913f), new VisemeMapping("lowerLipCurlOut", 0.146513f), new VisemeMapping("frownRight", 0.200000f), new VisemeMapping("frownLeft", 0.200000f), new VisemeMapping("sneerRight", 0.292479f), new VisemeMapping("sneerLeft", 0.357157f) } },
+
+ // Nasals
+ { "N", new[] { new VisemeMapping("jawOpen", 0.131241f), new VisemeMapping("smileRight", 0.256821f), new VisemeMapping("smileLeft", 0.223255f), new VisemeMapping("upperLipCurlOut", 0.133816f), new VisemeMapping("lowerLipCurlOut", 0.082580f), new VisemeMapping("sneerRight", 0.200000f), new VisemeMapping("sneerLeft", 0.200000f), new VisemeMapping("pucker", 0.070524f), new VisemeMapping("O_mouth", 0.076552f) } },
+ { "NG", new[] { new VisemeMapping("jawOpen", 0.088445f), new VisemeMapping("smileRight", 0.287764f), new VisemeMapping("smileLeft", 0.300000f), new VisemeMapping("upperLipCurlOut", 0.160000f), new VisemeMapping("sneerRight", 0.217440f), new VisemeMapping("sneerLeft", 0.234037f), new VisemeMapping("tongueUP", 1.000000f), new VisemeMapping("upperLipCurlin", 0.090000f), new VisemeMapping("jawForward", 0.067000f) } },
+
+ // Fricatives
+ { "F", new[] { new VisemeMapping("jawOpen", 0.130000f), new VisemeMapping("lowerLipCurlin", 0.400000f), new VisemeMapping("jawBack", 0.220000f) } },
+ { "V", new[] { new VisemeMapping("smileRight", 0.239000f), new VisemeMapping("smileLeft", 0.250000f), new VisemeMapping("jawOpen", 0.142000f), new VisemeMapping("frownRight", 0.183000f), new VisemeMapping("frownLeft", 0.180000f), new VisemeMapping("sneerRight", 0.197000f), new VisemeMapping("sneerLeft", 0.190000f), new VisemeMapping("lowerLipCurlin", 0.480000f), new VisemeMapping("jawBack", 0.220000f) } },
+ { "TH", new[] { new VisemeMapping("smileRight", 0.100000f), new VisemeMapping("smileLeft", 0.100000f), new VisemeMapping("jawOpen", 0.140000f), new VisemeMapping("upperLipCurlOut", 0.197000f), new VisemeMapping("lowerLipCurlOut", 0.070000f), new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.100000f), new VisemeMapping("sneerRight", 0.127000f), new VisemeMapping("sneerLeft", 0.124000f), new VisemeMapping("tongueUP", 1.000000f) } },
+ { "DH", new[] { new VisemeMapping("smileRight", 0.200000f), new VisemeMapping("smileLeft", 0.200000f), new VisemeMapping("jawOpen", 0.125088f), new VisemeMapping("upperLipCurlOut", 0.269000f), new VisemeMapping("frownRight", 0.200000f), new VisemeMapping("frownLeft", 0.200000f), new VisemeMapping("sneerRight", 0.300000f), new VisemeMapping("sneerLeft", 0.300000f), new VisemeMapping("O_mouth", 0.140000f), new VisemeMapping("lowerLipCurlin", 0.130000f), new VisemeMapping("tongueUP", 1.000000f), new VisemeMapping("upperLipCurlin", 0.080000f) } },
+ { "S", new[] { new VisemeMapping("smileRight", 0.100000f), new VisemeMapping("smileLeft", 0.100000f), new VisemeMapping("jawOpen", 0.165500f), new VisemeMapping("lowerLipCurlOut", 0.110000f), new VisemeMapping("sneerRight", 0.239000f), new VisemeMapping("sneerLeft", 0.240000f), new VisemeMapping("tongueUP", 1.000000f) } },
+ { "Z", new[] { new VisemeMapping("jawOpen", 0.120000f), new VisemeMapping("upperLipCurlOut", 0.250000f), new VisemeMapping("lowerLipCurlOut", 0.160000f), new VisemeMapping("frownLeft", 0.200000f), new VisemeMapping("sneerRight", 0.290000f), new VisemeMapping("upperLipCurlin", 0.150000f) } },
+ { "SH", new[] { new VisemeMapping("smileRight", 0.300000f), new VisemeMapping("smileLeft", 0.300000f), new VisemeMapping("jawOpen", 0.140000f), new VisemeMapping("upperLipCurlOut", 0.359000f), new VisemeMapping("lowerLipCurlOut", 0.130000f), new VisemeMapping("sneerRight", 0.300000f), new VisemeMapping("sneerLeft", 0.300000f) } },
+ { "ZH", new[] { new VisemeMapping("smileRight", 0.150000f), new VisemeMapping("smileLeft", 0.150000f), new VisemeMapping("jawOpen", 0.126000f), new VisemeMapping("upperLipCurlOut", 0.329000f), new VisemeMapping("lowerLipCurlOut", 0.188000f), new VisemeMapping("frownRight", 0.200000f), new VisemeMapping("frownLeft", 0.200000f), new VisemeMapping("sneerRight", 0.700000f), new VisemeMapping("sneerLeft", 0.700000f), new VisemeMapping("pucker", 0.120000f) } },
+ { "H", new[] { new VisemeMapping("smileRight", 0.300000f), new VisemeMapping("smileLeft", 0.300000f), new VisemeMapping("jawOpen", 0.270000f), new VisemeMapping("upperLipCurlOut", 0.170000f), new VisemeMapping("sneerRight", 0.350000f), new VisemeMapping("sneerLeft", 0.350000f), new VisemeMapping("O_mouth", 0.150000f), new VisemeMapping("tongueUP", 0.260000f) } },
+
+ // Approximants
+ { "R", new[] { new VisemeMapping("jawOpen", 0.140000f), new VisemeMapping("upperLipCurlOut", 0.120000f), new VisemeMapping("lowerLipCurlOut", 0.110000f), new VisemeMapping("pucker", 0.260000f), new VisemeMapping("tongueUP", 1.000000f) } },
+ { "L", new[] { new VisemeMapping("jawOpen", 0.180000f), new VisemeMapping("O_mouth", 0.090000f), new VisemeMapping("tongueUP", 1.000000f) } },
+ { "W", new[] { new VisemeMapping("jawOpen", 0.098700f), new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.100000f), new VisemeMapping("pucker", 0.160000f), new VisemeMapping("O_mouth", 0.390000f), new VisemeMapping("upperLipCurlin", 0.070000f) } },
+ { "Y", new[] { new VisemeMapping("smileRight", 0.150000f), new VisemeMapping("smileLeft", 0.150000f), new VisemeMapping("jawOpen", 0.100000f), new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.100000f), new VisemeMapping("sneerRight", 0.500000f), new VisemeMapping("sneerLeft", 0.500000f), new VisemeMapping("pucker", 0.330000f), new VisemeMapping("O_mouth", 0.150000f) } },
+
+ // Affricates
+ { "CH", new[] { new VisemeMapping("smileRight", 0.340000f), new VisemeMapping("smileLeft", 0.340000f), new VisemeMapping("jawOpen", 0.110000f), new VisemeMapping("lowerLipCurlOut", 0.115700f), new VisemeMapping("sneerRight", 0.330000f), new VisemeMapping("sneerLeft", 0.330000f), new VisemeMapping("O_mouth", 0.310000f), new VisemeMapping("upperLipCurlin", 0.200000f) } },
+ { "JH", new[] { new VisemeMapping("jawOpen", 0.060000f), new VisemeMapping("frownRight", 0.150000f), new VisemeMapping("frownLeft", 0.150000f), new VisemeMapping("sneerRight", 0.550000f), new VisemeMapping("sneerLeft", 0.550000f), new VisemeMapping("O_mouth", 0.150000f), new VisemeMapping("tongueUP", 1.000000f), new VisemeMapping("jawForward", 0.110000f) } },
+
+ // Flap
+ { "FLAP", new[] { new VisemeMapping("jawOpen", 0.098717f), new VisemeMapping("smileRight", 0.100000f), new VisemeMapping("smileLeft", 0.100000f), new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.100000f), new VisemeMapping("sneerRight", 0.100000f), new VisemeMapping("sneerLeft", 0.100000f), new VisemeMapping("tongueUP", 1.000000f) } },
+
+ // Special
+ { "TS", new[] { new VisemeMapping("smileRight", 0.150000f), new VisemeMapping("smileLeft", 0.150000f), new VisemeMapping("jawOpen", 0.150000f), new VisemeMapping("upperLipCurlOut", 0.150000f), new VisemeMapping("lowerLipCurlOut", 0.090000f), new VisemeMapping("sneerRight", 0.090000f), new VisemeMapping("sneerLeft", 0.080000f), new VisemeMapping("tongueUP", 1.000000f) } },
+
+ // Front vowels
+ { "IY", new[] { new VisemeMapping("smileRight", 0.200000f), new VisemeMapping("smileLeft", 0.200000f), new VisemeMapping("jawOpen", 0.110000f), new VisemeMapping("upperLipCurlOut", 0.230000f), new VisemeMapping("lowerLipCurlOut", 0.340000f) } },
+ { "IH", new[] { new VisemeMapping("smileRight", 0.180000f), new VisemeMapping("smileLeft", 0.180000f), new VisemeMapping("jawOpen", 0.300000f), new VisemeMapping("upperLipCurlOut", 0.260000f), new VisemeMapping("lowerLipCurlOut", 0.370000f) } },
+ { "EH", new[] { new VisemeMapping("smileRight", 0.250000f), new VisemeMapping("smileLeft", 0.250000f), new VisemeMapping("jawOpen", 0.300000f), new VisemeMapping("frownRight", 0.190000f), new VisemeMapping("frownLeft", 0.200000f), new VisemeMapping("sneerRight", 0.300000f), new VisemeMapping("sneerLeft", 0.300000f) } },
+ { "EY", new[] { new VisemeMapping("smileRight", 0.300000f), new VisemeMapping("smileLeft", 0.300000f), new VisemeMapping("jawOpen", 0.160000f), new VisemeMapping("lowerLipCurlOut", 0.150000f), new VisemeMapping("frownRight", 0.250000f), new VisemeMapping("frownLeft", 0.250000f), new VisemeMapping("sneerRight", 0.250000f), new VisemeMapping("sneerLeft", 0.250000f) } },
+ { "AE", new[] { new VisemeMapping("smileRight", 0.170000f), new VisemeMapping("smileLeft", 0.170000f), new VisemeMapping("jawOpen", 0.300000f), new VisemeMapping("lowerLipCurlOut", 0.140000f) } },
+
+ // Central vowels
+ { "AH", new[] { new VisemeMapping("jawOpen", 0.300000f), new VisemeMapping("pucker", 0.340000f), new VisemeMapping("O_mouth", 0.100000f) } },
+ { "AX", new[] { new VisemeMapping("smileRight", 0.250000f), new VisemeMapping("smileLeft", 0.200000f), new VisemeMapping("jawOpen", 0.170000f), new VisemeMapping("sneerRight", 0.100000f), new VisemeMapping("sneerLeft", 0.090000f) } },
+ { "ER", new[] { new VisemeMapping("smileRight", 0.100000f), new VisemeMapping("smileLeft", 0.100000f), new VisemeMapping("jawOpen", 0.140000f), new VisemeMapping("lowerLipCurlOut", 0.100000f), new VisemeMapping("O_mouth", 0.320000f), new VisemeMapping("tongueUP", 1.000000f), new VisemeMapping("jawBack", 0.270000f) } },
+
+ // Back vowels
+ { "UW", new[] { new VisemeMapping("jawOpen", 0.120000f), new VisemeMapping("pucker", 0.330000f), new VisemeMapping("O_mouth", 0.480000f) } },
+ { "UH", new[] { new VisemeMapping("jawOpen", 0.130000f), new VisemeMapping("pucker", 0.390000f), new VisemeMapping("O_mouth", 0.150000f), new VisemeMapping("lowerLipCurlin", 0.090000f) } },
+ { "OW", new[] { new VisemeMapping("jawOpen", 0.110144f), new VisemeMapping("sneerLeft", 0.320000f), new VisemeMapping("pucker", 0.250000f) } },
+ { "AA", new[] { new VisemeMapping("jawOpen", 0.180000f), new VisemeMapping("pucker", 0.220000f), new VisemeMapping("lowerLipCurlin", 0.220000f), new VisemeMapping("jawBack", 0.080000f) } },
+ { "AO", new[] { new VisemeMapping("jawOpen", 0.150000f), new VisemeMapping("frownRight", 0.130000f), new VisemeMapping("frownLeft", 0.140000f), new VisemeMapping("pucker", 0.060000f), new VisemeMapping("O_mouth", 0.280000f) } },
+
+ // Diphthongs
+ { "AY", new[] { new VisemeMapping("jawOpen", 0.200000f), new VisemeMapping("upperLipCurlOut", 0.200000f), new VisemeMapping("lowerLipCurlOut", 0.270000f), new VisemeMapping("sneerRight", 0.690000f) } },
+ { "AW", new[] { new VisemeMapping("jawOpen", 0.180000f), new VisemeMapping("frownRight", 0.200000f), new VisemeMapping("frownLeft", 0.200000f), new VisemeMapping("O_mouth", 0.250000f), new VisemeMapping("lowerLipCurlin", 0.250000f) } },
+ { "OY", new[] { new VisemeMapping("smileRight", 0.250000f), new VisemeMapping("smileLeft", 0.250000f), new VisemeMapping("jawOpen", 0.140000f), new VisemeMapping("upperLipCurlOut", 0.130000f), new VisemeMapping("frownRight", 0.120000f), new VisemeMapping("frownLeft", 0.120000f), new VisemeMapping("pucker", 0.210000f), new VisemeMapping("O_mouth", 0.270000f) } },
+ };
+
+ ///
+ /// All viseme animation names used in lip sync for Human Female
+ ///
+ public static readonly string[] HumanFemaleVisemes =
+ [
+ "m_Jaw+",
+ "m_Open",
+ "m_M",
+ "m_Jaw-",
+ "m_EE",
+ "m_N",
+ "m_G",
+ "m_OW",
+ "m_OH",
+ "m_Flap",
+ "m_FV",
+ "m_TH",
+ "m_L",
+ "m_ZZ",
+ "m_EH"
+ ];
+
+ ///
+ /// Human Male phoneme to viseme mappings - from HMM_HED_PROJoker_MDL_FaceFX data.
+ ///
+ public static readonly Dictionary HumanMalePhonemeMap = new()
+ {
+ // Silence
+ { "SIL", new[] { new VisemeMapping("m_Jaw+", 0.106584f), new VisemeMapping("m_Open", 0.161712f) } },
+
+ // Bilabial stops
+ { "P", new[] { new VisemeMapping("m_M", 0.729515f), new VisemeMapping("m_Jaw-", 0.112150f) } },
+ { "B", new[] { new VisemeMapping("m_M", 0.764770f), new VisemeMapping("m_Jaw-", 0.760000f) } },
+ { "M", new[] { new VisemeMapping("m_M", 1.000000f), new VisemeMapping("m_Jaw-", 0.186373f) } },
+
+ // Alveolar stops
+ { "T", new[] { new VisemeMapping("m_EE", 0.406647f), new VisemeMapping("m_N", 1.000000f), new VisemeMapping("m_G", 0.128312f) } },
+ { "D", new[] { new VisemeMapping("m_Jaw-", 0.054628f), new VisemeMapping("m_N", 1.000000f) } },
+
+ // Velar stops
+ { "K", new[] { new VisemeMapping("m_Jaw+", 0.250000f), new VisemeMapping("m_G", 0.363969f) } },
+ { "G", new[] { new VisemeMapping("m_Jaw+", 0.090000f), new VisemeMapping("m_G", 0.657148f) } },
+
+ // Nasals
+ { "N", new[] { new VisemeMapping("m_EE", 0.495714f), new VisemeMapping("m_OW", 0.495714f), new VisemeMapping("m_Jaw-", 0.110295f), new VisemeMapping("m_N", 1.000000f) } },
+ { "NG", new[] { new VisemeMapping("m_EE", 0.499425f), new VisemeMapping("m_Jaw+", 0.123284f), new VisemeMapping("m_M", 0.508703f), new VisemeMapping("m_N", 1.000000f) } },
+
+ // R variant
+ { "RU", new[] { new VisemeMapping("m_Jaw+", 0.110000f), new VisemeMapping("m_OH", 0.504991f) } },
+
+ // Flap
+ { "FLAP", new[] { new VisemeMapping("m_Flap", 1.000000f), new VisemeMapping("m_Jaw+", 0.219773f) } },
+
+ // Fricatives
+ { "F", new[] { new VisemeMapping("m_Jaw+", 0.128850f), new VisemeMapping("m_FV", 0.764770f) } },
+ { "V", new[] { new VisemeMapping("m_Jaw+", 0.151117f), new VisemeMapping("m_FV", 1.000000f) } },
+ { "TH", new[] { new VisemeMapping("m_Jaw+", 0.156684f), new VisemeMapping("m_TH", 0.723948f) } },
+ { "DH", new[] { new VisemeMapping("m_Jaw+", 0.216062f), new VisemeMapping("m_TH", 0.931771f) } },
+ { "S", new[] { new VisemeMapping("m_EE", 0.261913f), new VisemeMapping("m_Jaw+", 0.089884f), new VisemeMapping("m_OW", 0.614470f), new VisemeMapping("m_M", 0.471591f) } },
+ { "Z", new[] { new VisemeMapping("m_EE", 0.267479f), new VisemeMapping("m_Jaw+", 0.017517f), new VisemeMapping("m_OW", 0.571792f), new VisemeMapping("m_M", 0.647870f) } },
+ { "SH", new[] { new VisemeMapping("m_Jaw+", 0.090000f), new VisemeMapping("m_OW", 0.855693f) } },
+ { "ZH", new[] { new VisemeMapping("m_Jaw+", 0.089884f), new VisemeMapping("m_OW", 0.532825f) } },
+ { "HH", new[] { new VisemeMapping("m_EE", 0.352835f), new VisemeMapping("m_Jaw+", 0.164106f) } },
+ { "H", new[] { new VisemeMapping("m_EE", 0.352835f), new VisemeMapping("m_Jaw+", 0.164106f) } },
+
+ // Approximants
+ { "R", new[] { new VisemeMapping("m_Jaw+", 0.100000f), new VisemeMapping("m_OH", 0.569936f) } },
+ { "Y", new[] { new VisemeMapping("m_EE", 0.222946f), new VisemeMapping("m_Jaw+", 0.149262f) } },
+ { "L", new[] { new VisemeMapping("m_Jaw+", 0.151117f), new VisemeMapping("m_L", 1.000000f) } },
+ { "W", new[] { new VisemeMapping("m_Jaw+", 0.136273f), new VisemeMapping("m_OH", 0.709103f) } },
+
+ // Special
+ { "TS", new[] { new VisemeMapping("m_Jaw-", 0.065761f), new VisemeMapping("m_ZZ", 1.000000f) } },
+
+ // Affricates
+ { "CH", new[] { new VisemeMapping("m_Jaw+", 0.088028f), new VisemeMapping("m_Open", 1.000000f) } },
+ { "JH", new[] { new VisemeMapping("m_Jaw+", 0.125139f), new VisemeMapping("m_OH", 0.458602f) } },
+
+ // Front vowels
+ { "IY", new[] { new VisemeMapping("m_EE", 0.317580f), new VisemeMapping("m_Jaw+", 0.195651f), new VisemeMapping("m_ZZ", 0.822831f) } },
+ { "IH", new[] { new VisemeMapping("m_EE", 0.251318f), new VisemeMapping("m_Jaw+", 0.253173f), new VisemeMapping("m_OW", 0.336135f) } },
+ { "E", new[] { new VisemeMapping("m_Jaw+", 0.512952f), new VisemeMapping("m_EH", 0.466025f) } },
+ { "EH", new[] { new VisemeMapping("m_Jaw+", 0.247606f), new VisemeMapping("m_EH", 0.436336f) } },
+ { "EY", new[] { new VisemeMapping("m_Jaw+", 0.435018f), new VisemeMapping("m_Open", 0.304591f) } },
+ { "AE", new[] { new VisemeMapping("m_Jaw+", 0.423885f), new VisemeMapping("m_EH", 0.757348f) } },
+
+ // Central vowels
+ { "AH", new[] { new VisemeMapping("m_Jaw+", 0.429452f), new VisemeMapping("m_EH", 0.280468f), new VisemeMapping("m_OH", 0.085634f) } },
+ { "AX", new[] { new VisemeMapping("m_Jaw+", 0.453574f), new VisemeMapping("m_Open", 0.441902f) } },
+ { "ER", new[] { new VisemeMapping("m_Jaw+", 0.321829f), new VisemeMapping("m_Open", 0.547669f) } },
+ { "AXR", new[] { new VisemeMapping("m_Jaw+", 0.340385f), new VisemeMapping("m_Open", 0.820437f) } },
+ { "EXR", new[] { new VisemeMapping("m_Jaw+", 0.318118f), new VisemeMapping("m_Open", 0.336135f) } },
+
+ // Back vowels
+ { "UW", new[] { new VisemeMapping("m_Jaw+", 0.152973f), new VisemeMapping("m_OH", 0.863115f) } },
+ { "UH", new[] { new VisemeMapping("m_Jaw+", 0.264307f), new VisemeMapping("m_OH", 0.659003f) } },
+ { "OW", new[] { new VisemeMapping("m_Jaw+", 0.197506f), new VisemeMapping("m_OH", 0.569936f) } },
+ { "AA", new[] { new VisemeMapping("m_Jaw+", 0.459141f), new VisemeMapping("m_Open", 0.402936f), new VisemeMapping("m_OW", 0.402936f) } },
+ { "O", new[] { new VisemeMapping("m_Jaw+", 0.405329f), new VisemeMapping("m_OH", 0.460458f) } },
+ { "AO", new[] { new VisemeMapping("m_Jaw+", 0.405329f), new VisemeMapping("m_OH", 0.460458f) } },
+
+ // Diphthongs
+ { "AY", new[] { new VisemeMapping("m_Jaw+", 0.312551f) } },
+ { "AW", new[] { new VisemeMapping("m_Jaw+", 0.574185f), new VisemeMapping("m_Open", 0.683125f) } },
+ { "OY", new[] { new VisemeMapping("m_Jaw+", 0.260595f), new VisemeMapping("m_OH", 0.569936f) } },
+ };
+
+ ///
+ /// All viseme animation names used in lip sync for Human Male
+ ///
+ public static readonly string[] HumanMaleVisemes =
+ [
+ "m_Jaw+",
+ "m_Open",
+ "m_M",
+ "m_Jaw-",
+ "m_EE",
+ "m_N",
+ "m_G",
+ "m_OW",
+ "m_OH",
+ "m_Flap",
+ "m_FV",
+ "m_TH",
+ "m_L",
+ "m_ZZ",
+ "m_EH"
+ ];
+
+ ///
+ /// Asari phoneme to viseme mappings - from ASA_HED_PROBASE_MDL_FaceFX data.
+ ///
+ public static readonly Dictionary AsariPhonemeMap = new()
+ {
+ // Bilabial stops
+ { "P", new[] { new VisemeMapping("pucker", 0.270000f), new VisemeMapping("upperLipCurlIn", 0.330000f), new VisemeMapping("lowerLipCurlIn", 0.420000f), new VisemeMapping("jawClench", 0.240000f) } },
+ { "B", new[] { new VisemeMapping("upperLipCurlIn", 0.110000f), new VisemeMapping("lowerLipCurlIn", 0.110000f), new VisemeMapping("jawClench", 0.190000f) } },
+ { "M", new[] { new VisemeMapping("upperLipCurlIn", 0.433527f), new VisemeMapping("lowerLipCurlIn", 0.433527f), new VisemeMapping("jawBack", 0.154540f) } },
+
+ // Alveolar stops
+ { "T", new[] { new VisemeMapping("sneerRight", 0.050000f), new VisemeMapping("sneerLeft", 0.050000f), new VisemeMapping("frownRight", 0.512228f), new VisemeMapping("frownLeft", 0.520720f), new VisemeMapping("jawForward", 0.350883f) } },
+ { "D", new[] { new VisemeMapping("sneerRight", 0.020000f), new VisemeMapping("sneerLeft", 0.020000f), new VisemeMapping("frownRight", 0.350000f), new VisemeMapping("frownLeft", 0.350000f), new VisemeMapping("jawOpen", 0.040000f), new VisemeMapping("jawRotate", 0.060000f), new VisemeMapping("mouthDownLeft", 0.210000f), new VisemeMapping("mouthDownRight", 0.200000f), new VisemeMapping("tongueUP", 1.000000f) } },
+
+ // Velar stops
+ { "K", new[] { new VisemeMapping("frownRight", 0.350000f), new VisemeMapping("frownLeft", 0.350000f), new VisemeMapping("jawOpen", 0.048573f), new VisemeMapping("jawRotateDown", 0.080000f) } },
+ { "G", new[] { new VisemeMapping("frownRight", 0.420000f), new VisemeMapping("frownLeft", 0.420000f), new VisemeMapping("jawRotate", 0.320000f), new VisemeMapping("pucker", 0.130000f), new VisemeMapping("lowerLipCurlIn", 0.130000f), new VisemeMapping("upperLipCurlIn", 0.330000f) } },
+
+ // Nasals
+ { "N", new[] { new VisemeMapping("O_mouth", 0.230000f), new VisemeMapping("jawOpen", 0.070000f), new VisemeMapping("jawRotate", 0.030000f), new VisemeMapping("tongueUP", 1.000000f) } },
+ { "NG", new[] { new VisemeMapping("frownRight", 0.400000f), new VisemeMapping("frownLeft", 0.400000f), new VisemeMapping("jawOpen", 0.057065f), new VisemeMapping("jawRotateDown", 0.030000f), new VisemeMapping("lowerLipCurlIn", 0.260000f) } },
+
+ // R variants
+ { "RA", new[] { new VisemeMapping("O_mouth", 0.152174f), new VisemeMapping("jawOpen", 0.072351f), new VisemeMapping("jawRotate", 0.110000f), new VisemeMapping("jawRotateUp", 0.136889f) } },
+ { "RU", new[] { new VisemeMapping("jawRotate", 0.110000f), new VisemeMapping("jawOpen", 0.072351f), new VisemeMapping("O_mouth", 0.152174f), new VisemeMapping("jawRotateUp", 0.140000f) } },
+
+ // Flap
+ { "FLAP", new[] { new VisemeMapping("jawOpen", 0.005000f) } },
+
+ // Fricatives
+ { "PH", new[] { new VisemeMapping("sneerRight", 0.080000f), new VisemeMapping("sneerLeft", 0.080000f), new VisemeMapping("upperLipCurlOut", 0.547894f), new VisemeMapping("lowerLipCurlIn", 0.357989f), new VisemeMapping("jawClench", 0.082276f), new VisemeMapping("jawRotateUp", 0.128983f), new VisemeMapping("lowerLipUpLeft", 0.262568f), new VisemeMapping("lowerLipUpRight", 0.284647f) } },
+ { "F", new[] { new VisemeMapping("sneerRight", 0.080000f), new VisemeMapping("sneerLeft", 0.080000f), new VisemeMapping("upperLipCurlOut", 0.547894f), new VisemeMapping("lowerLipCurlIn", 0.357989f), new VisemeMapping("jawClench", 0.082276f), new VisemeMapping("jawRotateUp", 0.128983f), new VisemeMapping("lowerLipUpLeft", 0.262568f), new VisemeMapping("lowerLipUpRight", 0.284647f) } },
+ { "V", new[] { new VisemeMapping("upperLipCurlOut", 0.490000f), new VisemeMapping("lowerLipCurlIn", 0.216712f), new VisemeMapping("jawClench", 0.170000f) } },
+ { "TH", new[] { new VisemeMapping("O_mouth", 0.310000f), new VisemeMapping("jawOpen", 0.048573f), new VisemeMapping("jawRotate", 0.070000f), new VisemeMapping("jawRotateDown", 0.077446f), new VisemeMapping("tongueUP", 1.000000f) } },
+ { "DH", new[] { new VisemeMapping("upperLipCurlOut", 0.430000f), new VisemeMapping("jawOpen", 0.060000f), new VisemeMapping("jawRotate", 0.070000f), new VisemeMapping("tongueUP", 1.000000f) } },
+ { "S", new[] { new VisemeMapping("sneerRight", 0.080000f), new VisemeMapping("sneerLeft", 0.080000f), new VisemeMapping("frownRight", 0.423913f), new VisemeMapping("frownLeft", 0.432405f), new VisemeMapping("jawForward", 0.344090f), new VisemeMapping("smileRight", 0.020000f), new VisemeMapping("smileLeft", 0.020000f), new VisemeMapping("jawRotateUp", 0.094429f) } },
+ { "Z", new[] { new VisemeMapping("sneerRight", 0.063000f), new VisemeMapping("sneerLeft", 0.063000f), new VisemeMapping("frownRight", 0.445992f), new VisemeMapping("frownLeft", 0.452785f), new VisemeMapping("jawForward", 0.355978f), new VisemeMapping("smileRight", 0.040000f), new VisemeMapping("smileLeft", 0.040000f), new VisemeMapping("jawRotateUp", 0.079144f) } },
+ { "SH", new[] { new VisemeMapping("sneerRight", 0.030000f), new VisemeMapping("sneerLeft", 0.030000f), new VisemeMapping("frownLeft", 0.100000f), new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("O_mouth", 0.540000f) } },
+ { "ZH", new[] { new VisemeMapping("frownRight", 0.150000f), new VisemeMapping("frownLeft", 0.150000f), new VisemeMapping("O_mouth", 0.420000f), new VisemeMapping("jawOpen", 0.019701f), new VisemeMapping("pucker", 0.250000f), new VisemeMapping("jawRotateDown", 0.046875f) } },
+ { "CX", new[] { new VisemeMapping("frownRight", 0.350000f), new VisemeMapping("frownLeft", 0.350000f), new VisemeMapping("jawOpen", 0.048573f), new VisemeMapping("jawRotateDown", 0.080842f) } },
+ { "X", new[] { new VisemeMapping("frownRight", 0.350000f), new VisemeMapping("frownLeft", 0.350000f), new VisemeMapping("jawOpen", 0.048573f), new VisemeMapping("jawRotateDown", 0.080842f) } },
+ { "GH", new[] { new VisemeMapping("frownRight", 0.420000f), new VisemeMapping("frownLeft", 0.420000f), new VisemeMapping("jawRotate", 0.320000f), new VisemeMapping("pucker", 0.130000f), new VisemeMapping("lowerLipCurlIn", 0.130000f), new VisemeMapping("upperLipCurlIn", 0.330000f) } },
+ { "HH", new[] { new VisemeMapping("frownRight", 0.617527f), new VisemeMapping("frownLeft", 0.619226f), new VisemeMapping("jawOpen", 0.092708f), new VisemeMapping("jawRotate", 0.300000f), new VisemeMapping("pucker", 0.170000f), new VisemeMapping("jawRotateDown", 0.169158f) } },
+ { "H", new[] { new VisemeMapping("jawRotateDown", 0.169158f), new VisemeMapping("frownRight", 0.620000f), new VisemeMapping("frownLeft", 0.620000f), new VisemeMapping("jawOpen", 0.090000f), new VisemeMapping("jawRotate", 0.300000f), new VisemeMapping("pucker", 0.170000f) } },
+
+ // Approximants
+ { "R", new[] { new VisemeMapping("jawRotateUp", 0.136889f), new VisemeMapping("O_mouth", 0.152174f), new VisemeMapping("jawRotate", 0.110000f), new VisemeMapping("jawOpen", 0.072351f) } },
+ { "Y", new[] { new VisemeMapping("frownRight", 0.580163f), new VisemeMapping("frownLeft", 0.534307f), new VisemeMapping("jawRotate", 0.100000f), new VisemeMapping("jawRotateDown", 0.150000f), new VisemeMapping("jawOpen", 0.029891f) } },
+ { "L", new[] { new VisemeMapping("frownRight", 0.300000f), new VisemeMapping("frownLeft", 0.300000f), new VisemeMapping("jawRotateDown", 0.090000f), new VisemeMapping("tongueUP", 1.000000f), new VisemeMapping("jawRotate", 0.060462f), new VisemeMapping("jawOpen", 0.060000f) } },
+ { "W", new[] { new VisemeMapping("pucker", 0.760000f) } },
+
+ // Special
+ { "TS", new[] { new VisemeMapping("sneerRight", 0.050000f), new VisemeMapping("sneerLeft", 0.050000f), new VisemeMapping("frownRight", 0.512228f), new VisemeMapping("frownLeft", 0.520720f), new VisemeMapping("jawForward", 0.350883f) } },
+
+ // Affricates
+ { "CH", new[] { new VisemeMapping("sneerRight", 0.050000f), new VisemeMapping("sneerLeft", 0.050000f), new VisemeMapping("jawForward", 0.279552f), new VisemeMapping("frownRight", 0.200000f), new VisemeMapping("frownLeft", 0.200000f), new VisemeMapping("O_mouth", 0.442595f), new VisemeMapping("pucker", 0.116508f), new VisemeMapping("jawRotateDown", 0.062160f) } },
+ { "JH", new[] { new VisemeMapping("sneerRight", 0.020000f), new VisemeMapping("sneerLeft", 0.020000f), new VisemeMapping("frownRight", 0.200000f), new VisemeMapping("frownLeft", 0.200000f), new VisemeMapping("O_mouth", 0.300000f), new VisemeMapping("lowerLipCurlOut", 0.160000f), new VisemeMapping("jawForward", 0.340000f), new VisemeMapping("mouthDownLeft", 0.070000f), new VisemeMapping("mouthDownRight", 0.060000f) } },
+
+ // Front vowels
+ { "IY", new[] { new VisemeMapping("frownRight", 0.600000f), new VisemeMapping("frownLeft", 0.600000f), new VisemeMapping("jawOpen", 0.034986f), new VisemeMapping("smileLeft", 0.020000f), new VisemeMapping("smileRight", 0.020000f), new VisemeMapping("jawRotateDown", 0.123302f) } },
+ { "IH", new[] { new VisemeMapping("jawRotate", 0.100000f), new VisemeMapping("frownRight", 0.400000f), new VisemeMapping("frownLeft", 0.400000f), new VisemeMapping("jawOpen", 0.045177f), new VisemeMapping("smileLeft", 0.040000f), new VisemeMapping("smileRight", 0.040000f), new VisemeMapping("jawRotateDown", 0.128397f) } },
+ { "E", new[] { new VisemeMapping("frownRight", 0.300000f), new VisemeMapping("frownLeft", 0.300000f), new VisemeMapping("jawOpen", 0.063859f), new VisemeMapping("jawRotateDown", 0.102921f) } },
+ { "EN", new[] { new VisemeMapping("jawRotateDown", 0.102921f), new VisemeMapping("frownRight", 0.300000f), new VisemeMapping("frownLeft", 0.300000f), new VisemeMapping("jawOpen", 0.063859f) } },
+ { "EH", new[] { new VisemeMapping("sneerRight", 0.060000f), new VisemeMapping("sneerLeft", 0.060000f), new VisemeMapping("frownRight", 0.450000f), new VisemeMapping("frownLeft", 0.450000f), new VisemeMapping("jawOpen", 0.072917f), new VisemeMapping("jawRotate", 0.350000f), new VisemeMapping("jawRotateDown", 0.116508f) } },
+ { "EY", new[] { new VisemeMapping("frownRight", 0.600000f), new VisemeMapping("frownLeft", 0.600000f), new VisemeMapping("smileRight", 0.030000f), new VisemeMapping("smileLeft", 0.030000f), new VisemeMapping("jawRotateDown", 0.150476f), new VisemeMapping("jawOpen", 0.050000f) } },
+ { "AE", new[] { new VisemeMapping("frownRight", 0.300000f), new VisemeMapping("frownLeft", 0.300000f), new VisemeMapping("jawOpen", 0.053669f), new VisemeMapping("lowerLipCurlOut", 0.210000f), new VisemeMapping("jawRotateDown", 0.113111f) } },
+
+ // Central vowels
+ { "AH", new[] { new VisemeMapping("mouthDownLeft", 0.160000f), new VisemeMapping("mouthDownRight", 0.150000f), new VisemeMapping("O_mouth", 0.170000f), new VisemeMapping("jawOpen", 0.090000f), new VisemeMapping("jawRotate", 0.100000f) } },
+ { "AX", new[] { new VisemeMapping("jawRotateDown", 0.089334f), new VisemeMapping("frownRight", 0.400000f), new VisemeMapping("frownLeft", 0.400000f), new VisemeMapping("jawOpen", 0.065557f), new VisemeMapping("jawRotate", 0.100000f) } },
+ { "UX", new[] { new VisemeMapping("jawRotateDown", 0.089334f), new VisemeMapping("frownLeft", 0.400000f), new VisemeMapping("frownRight", 0.400000f), new VisemeMapping("jawOpen", 0.065557f), new VisemeMapping("jawRotate", 0.100000f) } },
+ { "ER", new[] { new VisemeMapping("jawRotateDown", 0.140285f), new VisemeMapping("jawClench", 0.125000f), new VisemeMapping("upperLipCurlIn", 0.190000f), new VisemeMapping("jawRotate", 0.160000f), new VisemeMapping("jawOpen", 0.021399f), new VisemeMapping("frownLeft", 0.575068f), new VisemeMapping("frownRight", 0.561481f) } },
+ { "AXR", new[] { new VisemeMapping("jawRotateDown", 0.140285f), new VisemeMapping("jawClench", 0.125000f), new VisemeMapping("upperLipCurlIn", 0.190000f), new VisemeMapping("jawRotate", 0.160000f), new VisemeMapping("jawOpen", 0.021399f), new VisemeMapping("frownLeft", 0.575068f), new VisemeMapping("frownRight", 0.561481f) } },
+ { "EXR", new[] { new VisemeMapping("jawRotateDown", 0.140285f), new VisemeMapping("jawClench", 0.125000f), new VisemeMapping("upperLipCurlIn", 0.190000f), new VisemeMapping("jawRotate", 0.160000f), new VisemeMapping("jawOpen", 0.021399f), new VisemeMapping("frownLeft", 0.575068f), new VisemeMapping("frownRight", 0.561481f) } },
+
+ // Back vowels
+ { "A", new[] { new VisemeMapping("jawRotateDown", 0.102921f), new VisemeMapping("frownRight", 0.300000f), new VisemeMapping("frownLeft", 0.300000f), new VisemeMapping("jawOpen", 0.063859f) } },
+ { "AA", new[] { new VisemeMapping("jawRotateDown", 0.102921f), new VisemeMapping("jawOpen", 0.063859f), new VisemeMapping("frownRight", 0.300000f), new VisemeMapping("frownLeft", 0.300000f) } },
+ { "AAN", new[] { new VisemeMapping("jawRotateDown", 0.102921f), new VisemeMapping("jawOpen", 0.063859f), new VisemeMapping("frownRight", 0.300000f), new VisemeMapping("frownLeft", 0.300000f) } },
+ { "AO", new[] { new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.100000f), new VisemeMapping("O_mouth", 0.125000f), new VisemeMapping("jawOpen", 0.053669f), new VisemeMapping("jawRotate", 0.100000f), new VisemeMapping("pucker", 0.074049f), new VisemeMapping("lowerLipCurlOut", 0.180000f), new VisemeMapping("mouthDownLeft", 0.170000f), new VisemeMapping("mouthDownRight", 0.140000f), new VisemeMapping("jawRotateDown", 0.090000f) } },
+ { "AON", new[] { new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.100000f), new VisemeMapping("O_mouth", 0.125000f), new VisemeMapping("jawOpen", 0.053669f), new VisemeMapping("jawRotate", 0.100000f), new VisemeMapping("pucker", 0.074049f), new VisemeMapping("lowerLipCurlOut", 0.180000f), new VisemeMapping("mouthDownLeft", 0.170000f), new VisemeMapping("mouthDownRight", 0.140000f), new VisemeMapping("jawRotateDown", 0.085938f) } },
+ { "O", new[] { new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.100000f), new VisemeMapping("O_mouth", 0.125000f), new VisemeMapping("jawOpen", 0.053669f), new VisemeMapping("jawRotate", 0.100000f), new VisemeMapping("pucker", 0.074049f), new VisemeMapping("lowerLipCurlOut", 0.180000f), new VisemeMapping("mouthDownLeft", 0.170000f), new VisemeMapping("mouthDownRight", 0.140000f), new VisemeMapping("jawRotateDown", 0.085938f) } },
+ { "ON", new[] { new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.100000f), new VisemeMapping("O_mouth", 0.125000f), new VisemeMapping("jawOpen", 0.053669f), new VisemeMapping("jawRotate", 0.100000f), new VisemeMapping("pucker", 0.074049f), new VisemeMapping("lowerLipCurlOut", 0.180000f), new VisemeMapping("mouthDownLeft", 0.170000f), new VisemeMapping("mouthDownRight", 0.140000f), new VisemeMapping("jawRotateDown", 0.085938f) } },
+ { "UW", new[] { new VisemeMapping("jawRotateDown", 0.125000f), new VisemeMapping("O_mouth", 0.264266f), new VisemeMapping("jawRotate", 0.140000f), new VisemeMapping("jawOpen", 0.109375f), new VisemeMapping("lowerLipCurlOut", 0.570000f) } },
+ { "UH", new[] { new VisemeMapping("jawRotateDown", 0.080842f), new VisemeMapping("O_mouth", 0.370000f), new VisemeMapping("upperLipCurlOut", 0.220000f), new VisemeMapping("jawOpen", 0.040000f), new VisemeMapping("jawRotate", 0.150000f), new VisemeMapping("lowerLipCurlOut", 0.510000f) } },
+ { "OW", new[] { new VisemeMapping("jawRotateDown", 0.082541f), new VisemeMapping("O_mouth", 0.310000f), new VisemeMapping("jawRotate", 0.100000f), new VisemeMapping("jawOpen", 0.038383f), new VisemeMapping("pucker", 0.270000f) } },
+ { "UY", new[] { new VisemeMapping("pucker", 0.760000f) } },
+ { "UU", new[] { new VisemeMapping("pucker", 0.760000f) } },
+ { "EU", new[] { new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.100000f), new VisemeMapping("O_mouth", 0.125000f), new VisemeMapping("jawOpen", 0.053669f), new VisemeMapping("jawRotate", 0.100000f), new VisemeMapping("pucker", 0.074049f), new VisemeMapping("lowerLipCurlOut", 0.180000f), new VisemeMapping("mouthDownLeft", 0.170000f), new VisemeMapping("mouthDownRight", 0.140000f), new VisemeMapping("jawRotateDown", 0.085938f) } },
+ { "OE", new[] { new VisemeMapping("frownLeft", 0.100000f), new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("O_mouth", 0.125000f), new VisemeMapping("jawOpen", 0.053669f), new VisemeMapping("jawRotate", 0.100000f), new VisemeMapping("pucker", 0.074049f), new VisemeMapping("lowerLipCurlOut", 0.180000f), new VisemeMapping("mouthDownLeft", 0.170000f), new VisemeMapping("mouthDownRight", 0.140000f), new VisemeMapping("jawRotateDown", 0.085938f) } },
+ { "OEN", new[] { new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.100000f), new VisemeMapping("O_mouth", 0.125000f), new VisemeMapping("jawOpen", 0.053669f), new VisemeMapping("jawRotate", 0.100000f), new VisemeMapping("pucker", 0.074049f), new VisemeMapping("lowerLipCurlOut", 0.180000f), new VisemeMapping("mouthDownLeft", 0.170000f), new VisemeMapping("mouthDownRight", 0.140000f), new VisemeMapping("jawRotateDown", 0.085938f) } },
+
+ // Diphthongs
+ { "AY", new[] { new VisemeMapping("frownRight", 0.400000f), new VisemeMapping("frownLeft", 0.400000f), new VisemeMapping("jawOpen", 0.065557f), new VisemeMapping("jawRotate", 0.100000f), new VisemeMapping("smileRight", 0.050000f), new VisemeMapping("smileLeft", 0.038383f), new VisemeMapping("jawRotateDown", 0.085938f) } },
+ { "AW", new[] { new VisemeMapping("jawRotateDown", 0.097826f), new VisemeMapping("jawOpen", 0.040000f), new VisemeMapping("jawRotate", 0.350000f), new VisemeMapping("mouthDownRight", 0.120000f), new VisemeMapping("mouthDownLeft", 0.140000f), new VisemeMapping("O_mouth", 0.408628f) } },
+ { "OY", new[] { new VisemeMapping("jawRotateDown", 0.102921f), new VisemeMapping("O_mouth", 0.371264f), new VisemeMapping("jawOpen", 0.048573f), new VisemeMapping("jawRotate", 0.300000f) } },
+ };
+
+ ///
+ /// All viseme animation names used in lip sync for Asari
+ ///
+ public static readonly string[] AsariVisemes =
+ [
+ "pucker",
+ "upperLipCurlIn",
+ "lowerLipCurlIn",
+ "jawClench",
+ "sneerRight",
+ "sneerLeft",
+ "frownRight",
+ "frownLeft",
+ "jawForward",
+ "jawOpen",
+ "jawRotate",
+ "jawRotateDown",
+ "jawRotateUp",
+ "mouthDownLeft",
+ "mouthDownRight",
+ "tongueUP",
+ "O_mouth",
+ "upperLipCurlOut",
+ "lowerLipCurlOut",
+ "smileRight",
+ "smileLeft",
+ "jawBack",
+ "lowerLipUpLeft",
+ "lowerLipUpRight"
+ ];
+
+ ///
+ /// All viseme animation names used in lip sync for Krogan
+ ///
+ public static readonly string[] KroganVisemes =
+ [
+ "jawOpen",
+ "smileRight",
+ "smileLeft",
+ "sneerRight",
+ "sneerLeft",
+ "lowerLipCurlin",
+ "upperLipCurlin",
+ "jawBack",
+ "upperLipCurlOut",
+ "lowerLipCurlOut",
+ "tongueUP",
+ "pucker",
+ "O_mouth",
+ "frownRight",
+ "frownLeft",
+ "jawForward"
+ ];
+
+ ///
+ /// Drell phoneme to viseme mappings - from DRL_HED_PROTHANE_MDL_FaceFX data.
+ ///
+ public static readonly Dictionary DrellPhonemeMap = new()
+ {
+ // Alveolar stops
+ { "T", new[] { new VisemeMapping("smileRight", 0.040000f), new VisemeMapping("smileLeft", 0.040000f), new VisemeMapping("sneerRight", 0.100000f), new VisemeMapping("sneerLeft", 0.120000f), new VisemeMapping("frownRight", 0.200000f), new VisemeMapping("frownLeft", 0.200000f), new VisemeMapping("jawForward", 0.350000f) } },
+ { "D", new[] { new VisemeMapping("sneerLeft", 0.100000f), new VisemeMapping("sneerRight", 0.110000f), new VisemeMapping("frownRight", 0.300000f), new VisemeMapping("frownLeft", 0.300000f), new VisemeMapping("jawRotate", 0.060000f), new VisemeMapping("jawOpen", 0.040000f), new VisemeMapping("mouthDownLeft", 0.210000f), new VisemeMapping("tongueUP", 1.000000f), new VisemeMapping("mouthDownRight", 0.200000f) } },
+
+ // Velar stops
+ { "K", new[] { new VisemeMapping("frownRight", 0.200000f), new VisemeMapping("frownLeft", 0.200000f), new VisemeMapping("jawOpen", 0.050000f) } },
+ { "G", new[] { new VisemeMapping("sneerRight", 0.113839f), new VisemeMapping("sneerLeft", 0.087798f), new VisemeMapping("frownRight", 0.348214f), new VisemeMapping("frownLeft", 0.340774f), new VisemeMapping("jawRotate", 0.220000f), new VisemeMapping("pucker", 0.130000f), new VisemeMapping("upperLipCurlIn", 0.330000f), new VisemeMapping("lowerLipCurlIn", 0.046875f), new VisemeMapping("jawRotateUp", 0.229167f) } },
+
+ // Bilabial stops
+ { "P", new[] { new VisemeMapping("pucker", 0.050000f), new VisemeMapping("upperLipCurlIn", 0.330000f), new VisemeMapping("jawClench", 0.240000f), new VisemeMapping("lowerLipCurlIn", 0.420000f) } },
+ { "B", new[] { new VisemeMapping("upperLipCurlIn", 0.110000f), new VisemeMapping("lowerLipCurlIn", 0.110000f), new VisemeMapping("jawClench", 0.190000f) } },
+ { "M", new[] { new VisemeMapping("upperLipCurlIn", 0.690000f), new VisemeMapping("lowerLipCurlIn", 0.890000f) } },
+
+ // Nasals
+ { "N", new[] { new VisemeMapping("sneerRight", 0.113957f), new VisemeMapping("sneerLeft", 0.118523f), new VisemeMapping("jawRotateUp", 0.220107f), new VisemeMapping("tongueUP", 1.000000f), new VisemeMapping("jawRotate", 0.030000f), new VisemeMapping("O_mouth", 0.100000f), new VisemeMapping("jawOpen", 0.070000f) } },
+ { "NG", new[] { new VisemeMapping("frownRight", 0.200000f), new VisemeMapping("frownLeft", 0.200000f), new VisemeMapping("jawOpen", 0.060000f), new VisemeMapping("lowerLipCurlIn", 0.260000f) } },
+
+ // R variants
+ { "RA", new[] { new VisemeMapping("jawRotate", 0.110000f), new VisemeMapping("jawOpen", 0.040000f), new VisemeMapping("O_mouth", 0.200000f) } },
+ { "RU", new[] { new VisemeMapping("jawRotate", 0.110000f), new VisemeMapping("jawOpen", 0.040000f), new VisemeMapping("O_mouth", 0.200000f) } },
+
+ // Flap
+ { "FLAP", new[] { new VisemeMapping("jawOpen", 0.005000f) } },
+
+ // Fricatives
+ { "PH", new[] { new VisemeMapping("sneerRight", 0.070000f), new VisemeMapping("sneerLeft", 0.060000f), new VisemeMapping("upperLipCurlOut", 0.520000f), new VisemeMapping("lowerLipCurlIn", 1.000000f) } },
+ { "F", new[] { new VisemeMapping("sneerRight", 0.070000f), new VisemeMapping("sneerLeft", 0.060000f), new VisemeMapping("upperLipCurlOut", 0.520000f), new VisemeMapping("lowerLipCurlIn", 1.000000f) } },
+ { "V", new[] { new VisemeMapping("sneerRight", 0.060000f), new VisemeMapping("sneerLeft", 0.040000f), new VisemeMapping("lowerLipCurlIn", 1.000000f), new VisemeMapping("upperLipCurlOut", 0.490000f), new VisemeMapping("jawClench", 0.170000f) } },
+ { "TH", new[] { new VisemeMapping("sneerRight", 0.173310f), new VisemeMapping("sneerLeft", 0.164179f), new VisemeMapping("jawRotate", 0.053463f), new VisemeMapping("O_mouth", 0.200000f), new VisemeMapping("jawOpen", 0.039766f), new VisemeMapping("jawRotateUp", 0.210976f), new VisemeMapping("tongueUP", 1.000000f) } },
+ { "DH", new[] { new VisemeMapping("jawRotate", 0.070000f), new VisemeMapping("O_mouth", 0.100000f), new VisemeMapping("jawOpen", 0.060000f), new VisemeMapping("upperLipCurlOut", 0.430000f), new VisemeMapping("tongueUP", 1.000000f), new VisemeMapping("jawRotateUp", 0.119664f) } },
+ { "S", new[] { new VisemeMapping("smileRight", 0.050000f), new VisemeMapping("smileLeft", 0.050000f), new VisemeMapping("sneerRight", 0.180000f), new VisemeMapping("sneerLeft", 0.210000f), new VisemeMapping("frownLeft", 0.300000f), new VisemeMapping("frownRight", 0.300000f), new VisemeMapping("jawForward", 0.020000f) } },
+ { "Z", new[] { new VisemeMapping("smileRight", 0.060000f), new VisemeMapping("smileLeft", 0.060000f), new VisemeMapping("jawForward", 0.220000f), new VisemeMapping("sneerRight", 0.130000f), new VisemeMapping("sneerLeft", 0.150000f), new VisemeMapping("frownRight", 0.200000f), new VisemeMapping("frownLeft", 0.200000f) } },
+ { "SH", new[] { new VisemeMapping("sneerRight", 0.120000f), new VisemeMapping("sneerLeft", 0.100000f), new VisemeMapping("frownLeft", 0.100000f), new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("O_mouth", 0.300000f) } },
+ { "ZH", new[] { new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.100000f), new VisemeMapping("sneerRight", 0.070000f), new VisemeMapping("sneerLeft", 0.070000f), new VisemeMapping("O_mouth", 0.200000f), new VisemeMapping("pucker", 0.050000f), new VisemeMapping("lowerLipCurlOut", 0.300000f), new VisemeMapping("jawForward", 0.340000f) } },
+ { "X", new[] { new VisemeMapping("frownRight", 0.200000f), new VisemeMapping("frownLeft", 0.200000f), new VisemeMapping("jawOpen", 0.050000f) } },
+ { "CX", new[] { new VisemeMapping("frownRight", 0.200000f), new VisemeMapping("frownLeft", 0.200000f), new VisemeMapping("jawOpen", 0.050000f) } },
+ { "GH", new[] { new VisemeMapping("sneerRight", 0.113839f), new VisemeMapping("sneerLeft", 0.087798f), new VisemeMapping("frownRight", 0.348214f), new VisemeMapping("frownLeft", 0.340774f), new VisemeMapping("jawRotate", 0.220000f), new VisemeMapping("pucker", 0.130000f), new VisemeMapping("upperLipCurlIn", 0.330000f), new VisemeMapping("lowerLipCurlIn", 0.046875f), new VisemeMapping("jawRotateUp", 0.229167f) } },
+ { "HH", new[] { new VisemeMapping("frownRight", 0.300000f), new VisemeMapping("frownLeft", 0.300000f), new VisemeMapping("jawRotate", 0.190000f), new VisemeMapping("pucker", 0.050000f) } },
+ { "H", new[] { new VisemeMapping("frownRight", 0.300000f), new VisemeMapping("frownLeft", 0.300000f), new VisemeMapping("jawRotate", 0.190000f), new VisemeMapping("pucker", 0.050000f) } },
+
+ // Approximants
+ { "R", new[] { new VisemeMapping("jawRotate", 0.110000f), new VisemeMapping("O_mouth", 0.200000f), new VisemeMapping("jawOpen", 0.040000f) } },
+ { "Y", new[] { new VisemeMapping("frownRight", 0.300000f), new VisemeMapping("frownLeft", 0.300000f), new VisemeMapping("jawRotate", 0.100000f) } },
+ { "L", new[] { new VisemeMapping("frownRight", 0.200000f), new VisemeMapping("frownLeft", 0.200000f), new VisemeMapping("jawRotate", 0.060000f), new VisemeMapping("jawOpen", 0.040000f), new VisemeMapping("tongueUP", 1.000000f) } },
+ { "W", new[] { new VisemeMapping("pucker", 0.300000f) } },
+
+ // Special
+ { "TS", new[] { new VisemeMapping("smileRight", 0.040000f), new VisemeMapping("smileLeft", 0.040000f), new VisemeMapping("sneerRight", 0.100000f), new VisemeMapping("sneerLeft", 0.120000f), new VisemeMapping("frownLeft", 0.200000f), new VisemeMapping("frownRight", 0.200000f), new VisemeMapping("jawForward", 0.350000f) } },
+
+ // Affricates
+ { "CH", new[] { new VisemeMapping("sneerRight", 0.090000f), new VisemeMapping("sneerLeft", 0.090000f), new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.070000f), new VisemeMapping("O_mouth", 0.100000f), new VisemeMapping("pucker", 0.050000f) } },
+ { "JH", new[] { new VisemeMapping("sneerRight", 0.130000f), new VisemeMapping("sneerLeft", 0.100000f), new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.100000f), new VisemeMapping("O_mouth", 0.100000f), new VisemeMapping("lowerLipCurlOut", 0.160000f), new VisemeMapping("jawForward", 0.340000f), new VisemeMapping("mouthDownLeft", 0.070000f), new VisemeMapping("mouthDownRight", 0.060000f) } },
+
+ // Front vowels
+ { "IY", new[] { new VisemeMapping("smileRight", 0.118176f), new VisemeMapping("smileLeft", 0.104346f), new VisemeMapping("sneerRight", 0.080000f), new VisemeMapping("frownRight", 0.300000f), new VisemeMapping("frownLeft", 0.300000f), new VisemeMapping("sneerLeft", 0.090000f), new VisemeMapping("jawRotate", 0.160000f), new VisemeMapping("jawOpen", 0.020000f), new VisemeMapping("mouthDownLeft", 0.210000f), new VisemeMapping("mouthDownRight", 0.220000f) } },
+ { "IH", new[] { new VisemeMapping("smileRight", 0.040000f), new VisemeMapping("smileLeft", 0.040000f), new VisemeMapping("frownRight", 0.300000f), new VisemeMapping("frownLeft", 0.300000f), new VisemeMapping("jawOpen", 0.030000f), new VisemeMapping("jawRotate", 0.200000f), new VisemeMapping("mouthDownLeft", 0.230000f), new VisemeMapping("mouthDownRight", 0.210000f) } },
+ { "EH", new[] { new VisemeMapping("smileRight", 0.070000f), new VisemeMapping("smileLeft", 0.070000f), new VisemeMapping("sneerRight", 0.120000f), new VisemeMapping("sneerLeft", 0.110000f), new VisemeMapping("frownRight", 0.200000f), new VisemeMapping("frownLeft", 0.200000f), new VisemeMapping("jawRotate", 0.250000f) } },
+ { "EY", new[] { new VisemeMapping("jawRotate", 0.160000f), new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.100000f), new VisemeMapping("jawOpen", 0.070000f), new VisemeMapping("mouthDownLeft", 0.220000f), new VisemeMapping("mouthDownRight", 0.210000f), new VisemeMapping("smileRight", 0.081651f), new VisemeMapping("smileLeft", 0.131740f) } },
+ { "AE", new[] { new VisemeMapping("jawRotate", 0.060000f), new VisemeMapping("jawOpen", 0.080000f), new VisemeMapping("lowerLipCurlOut", 0.270000f), new VisemeMapping("mouthDownLeft", 0.180000f), new VisemeMapping("mouthDownRight", 0.190000f) } },
+ { "E", new[] { new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.100000f), new VisemeMapping("jawRotate", 0.060000f), new VisemeMapping("jawOpen", 0.089988f), new VisemeMapping("mouthDownLeft", 0.170000f), new VisemeMapping("mouthDownRight", 0.140000f) } },
+ { "EN", new[] { new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.100000f), new VisemeMapping("jawRotate", 0.060000f), new VisemeMapping("jawOpen", 0.089988f), new VisemeMapping("mouthDownLeft", 0.170000f), new VisemeMapping("mouthDownRight", 0.140000f) } },
+
+ // Central vowels
+ { "AH", new[] { new VisemeMapping("jawRotate", 0.190000f), new VisemeMapping("O_mouth", 0.100000f), new VisemeMapping("jawOpen", 0.090000f), new VisemeMapping("mouthDownLeft", 0.160000f), new VisemeMapping("mouthDownRight", 0.150000f) } },
+ { "AX", new[] { new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.100000f), new VisemeMapping("jawRotate", 0.100000f), new VisemeMapping("jawOpen", 0.060000f) } },
+ { "UX", new[] { new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.100000f), new VisemeMapping("jawRotate", 0.100000f), new VisemeMapping("jawOpen", 0.060000f) } },
+ { "ER", new[] { new VisemeMapping("smileLeft", 0.040000f), new VisemeMapping("frownRight", 0.200000f), new VisemeMapping("frownLeft", 0.200000f), new VisemeMapping("jawRotate", 0.160000f), new VisemeMapping("jawOpen", 0.040000f), new VisemeMapping("upperLipCurlIn", 0.190000f), new VisemeMapping("jawClench", 0.280000f) } },
+ { "AXR", new[] { new VisemeMapping("smileLeft", 0.040000f), new VisemeMapping("frownRight", 0.200000f), new VisemeMapping("frownLeft", 0.200000f), new VisemeMapping("jawRotate", 0.160000f), new VisemeMapping("jawOpen", 0.040000f), new VisemeMapping("upperLipCurlIn", 0.190000f), new VisemeMapping("jawClench", 0.280000f) } },
+ { "EXR", new[] { new VisemeMapping("smileLeft", 0.040000f), new VisemeMapping("frownRight", 0.200000f), new VisemeMapping("frownLeft", 0.200000f), new VisemeMapping("jawRotate", 0.160000f), new VisemeMapping("jawOpen", 0.040000f), new VisemeMapping("upperLipCurlIn", 0.190000f), new VisemeMapping("jawClench", 0.280000f) } },
+
+ // Back vowels
+ { "UW", new[] { new VisemeMapping("jawRotate", 0.140000f), new VisemeMapping("jawOpen", 0.050000f), new VisemeMapping("O_mouth", 0.363923f), new VisemeMapping("pucker", 0.099119f), new VisemeMapping("lowerLipCurlOut", 0.570000f) } },
+ { "UH", new[] { new VisemeMapping("jawRotate", 0.150000f), new VisemeMapping("jawOpen", 0.040000f), new VisemeMapping("O_mouth", 0.200000f), new VisemeMapping("upperLipCurlOut", 0.220000f), new VisemeMapping("lowerLipCurlOut", 0.510000f) } },
+ { "OW", new[] { new VisemeMapping("jawRotate", 0.190000f), new VisemeMapping("O_mouth", 0.200000f), new VisemeMapping("jawOpen", 0.060000f), new VisemeMapping("pucker", 0.050000f) } },
+ { "AA", new[] { new VisemeMapping("jawOpen", 0.089988f), new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.100000f), new VisemeMapping("jawRotate", 0.060000f), new VisemeMapping("mouthDownLeft", 0.170000f), new VisemeMapping("mouthDownRight", 0.140000f) } },
+ { "A", new[] { new VisemeMapping("jawOpen", 0.089988f), new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.100000f), new VisemeMapping("jawRotate", 0.060000f), new VisemeMapping("mouthDownLeft", 0.170000f), new VisemeMapping("mouthDownRight", 0.140000f) } },
+ { "AAN", new[] { new VisemeMapping("jawOpen", 0.089988f), new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.100000f), new VisemeMapping("jawRotate", 0.060000f), new VisemeMapping("mouthDownLeft", 0.170000f), new VisemeMapping("mouthDownRight", 0.140000f) } },
+ { "AO", new[] { new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.100000f), new VisemeMapping("jawRotate", 0.140000f), new VisemeMapping("jawOpen", 0.080000f), new VisemeMapping("mouthDownLeft", 0.170000f), new VisemeMapping("mouthDownRight", 0.140000f), new VisemeMapping("pucker", 0.050000f), new VisemeMapping("lowerLipCurlOut", 0.180000f) } },
+ { "AON", new[] { new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.100000f), new VisemeMapping("jawRotate", 0.140000f), new VisemeMapping("jawOpen", 0.080000f), new VisemeMapping("mouthDownLeft", 0.170000f), new VisemeMapping("mouthDownRight", 0.140000f), new VisemeMapping("pucker", 0.050000f), new VisemeMapping("lowerLipCurlOut", 0.180000f) } },
+ { "O", new[] { new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.100000f), new VisemeMapping("jawRotate", 0.140000f), new VisemeMapping("jawOpen", 0.080000f), new VisemeMapping("mouthDownLeft", 0.170000f), new VisemeMapping("mouthDownRight", 0.140000f), new VisemeMapping("pucker", 0.050000f), new VisemeMapping("lowerLipCurlOut", 0.180000f) } },
+ { "ON", new[] { new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.100000f), new VisemeMapping("jawRotate", 0.140000f), new VisemeMapping("jawOpen", 0.080000f), new VisemeMapping("mouthDownLeft", 0.170000f), new VisemeMapping("mouthDownRight", 0.140000f), new VisemeMapping("pucker", 0.050000f), new VisemeMapping("lowerLipCurlOut", 0.180000f) } },
+ { "UY", new[] { new VisemeMapping("pucker", 0.300000f) } },
+ { "UU", new[] { new VisemeMapping("pucker", 0.300000f) } },
+ { "EU", new[] { new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.100000f), new VisemeMapping("jawRotate", 0.140000f), new VisemeMapping("jawOpen", 0.080000f), new VisemeMapping("pucker", 0.050000f), new VisemeMapping("lowerLipCurlOut", 0.180000f), new VisemeMapping("mouthDownLeft", 0.170000f), new VisemeMapping("mouthDownRight", 0.140000f) } },
+ { "OE", new[] { new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.100000f), new VisemeMapping("jawRotate", 0.140000f), new VisemeMapping("jawOpen", 0.080000f), new VisemeMapping("pucker", 0.050000f), new VisemeMapping("lowerLipCurlOut", 0.180000f), new VisemeMapping("mouthDownLeft", 0.170000f), new VisemeMapping("mouthDownRight", 0.140000f) } },
+ { "OEN", new[] { new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.100000f), new VisemeMapping("jawRotate", 0.140000f), new VisemeMapping("jawOpen", 0.080000f), new VisemeMapping("pucker", 0.050000f), new VisemeMapping("lowerLipCurlOut", 0.180000f), new VisemeMapping("mouthDownLeft", 0.170000f), new VisemeMapping("mouthDownRight", 0.140000f) } },
+
+ // Diphthongs
+ { "AY", new[] { new VisemeMapping("frownRight", 0.200000f), new VisemeMapping("frownLeft", 0.200000f), new VisemeMapping("jawOpen", 0.010000f), new VisemeMapping("jawRotate", 0.160000f), new VisemeMapping("mouthDownLeft", 0.230000f), new VisemeMapping("mouthDownRight", 0.220000f) } },
+ { "AW", new[] { new VisemeMapping("jawRotate", 0.240000f), new VisemeMapping("O_mouth", 0.300000f), new VisemeMapping("jawOpen", 0.040000f), new VisemeMapping("mouthDownLeft", 0.140000f), new VisemeMapping("mouthDownRight", 0.120000f) } },
+ { "OY", new[] { new VisemeMapping("O_mouth", 0.300000f), new VisemeMapping("jawOpen", 0.080000f), new VisemeMapping("jawRotate", 0.210000f) } },
+ };
+
+ ///
+ /// All viseme animation names used in lip sync for Drell
+ ///
+ public static readonly string[] DrellVisemes =
+ [
+ "smileRight",
+ "smileLeft",
+ "sneerRight",
+ "sneerLeft",
+ "frownRight",
+ "frownLeft",
+ "jawForward",
+ "jawRotate",
+ "jawOpen",
+ "mouthDownLeft",
+ "mouthDownRight",
+ "tongueUP",
+ "pucker",
+ "upperLipCurlIn",
+ "lowerLipCurlIn",
+ "jawClench",
+ "jawRotateUp",
+ "O_mouth",
+ "upperLipCurlOut",
+ "lowerLipCurlOut"
+ ];
+
+ ///
+ /// Turian phoneme to viseme mappings - from TUR_HED_PROGarrus_MDL_FaceFX data.
+ ///
+ public static readonly Dictionary TurianPhonemeMap = new()
+ {
+ // Silence
+ { "SIL", new[] { new VisemeMapping("jawClench", 0.010000f) } },
+
+ // Bilabial stops
+ { "P", new[] { new VisemeMapping("lowerLipCurlin", 0.760000f), new VisemeMapping("pucker", 0.760000f), new VisemeMapping("upperLipCurlin", 0.760000f), new VisemeMapping("noseDown", 0.020000f) } },
+ { "B", new[] { new VisemeMapping("jawClench", 0.130599f) } },
+ { "M", new[] { new VisemeMapping("upperLipCurlin", 1.000000f), new VisemeMapping("lowerLipCurlin", 1.000000f), new VisemeMapping("pucker", 1.000000f), new VisemeMapping("smileRight", 0.430000f), new VisemeMapping("smileLeft", 0.430000f) } },
+
+ // Alveolar stops
+ { "T", new[] { new VisemeMapping("jawOpen", 0.030000f), new VisemeMapping("upperLipCurlin", 0.760000f), new VisemeMapping("lowerLipCurlin", 0.760000f), new VisemeMapping("pucker", 0.170000f), new VisemeMapping("noseDown", 0.210000f), new VisemeMapping("tongueUp2", 0.505000f), new VisemeMapping("tongueUp3", 0.809000f) } },
+ { "D", new[] { new VisemeMapping("upperLipCurlOut", 0.370000f), new VisemeMapping("lowerLipCurlOut", 0.280000f), new VisemeMapping("jawOpen", 0.050000f), new VisemeMapping("sneerRight", 0.130000f), new VisemeMapping("sneerLeft", 0.130000f), new VisemeMapping("tongueUp1", 0.537000f), new VisemeMapping("tongueUp2", 0.774000f), new VisemeMapping("tongueUp3", 0.280000f) } },
+
+ // Velar stops
+ { "K", new[] { new VisemeMapping("upperLipCurlOut", 0.350000f), new VisemeMapping("lowerLipCurlOut", 0.390000f), new VisemeMapping("O_mouth", 0.080000f), new VisemeMapping("sneerRight", 0.100000f), new VisemeMapping("sneerLeft", 0.100000f) } },
+ { "G", new[] { new VisemeMapping("upperLipCurlOut", 0.500000f), new VisemeMapping("lowerLipCurlOut", 0.520000f), new VisemeMapping("O_mouth", 0.160000f), new VisemeMapping("jawOpen", 0.010000f) } },
+
+ // Nasals
+ { "N", new[] { new VisemeMapping("sneerRight", 0.250000f), new VisemeMapping("sneerLeft", 0.250000f), new VisemeMapping("tongueUp1", 0.510000f), new VisemeMapping("tongueUp2", 0.530000f), new VisemeMapping("tongueUp3", 0.700000f) } },
+ { "NG", new[] { new VisemeMapping("upperLipCurlin", 0.420000f), new VisemeMapping("tongueUp", 0.720000f), new VisemeMapping("smileRight", 0.690000f), new VisemeMapping("smileLeft", 0.690000f), new VisemeMapping("sneerRight", 0.200000f), new VisemeMapping("sneerLeft", 0.200000f) } },
+
+ // R variants
+ { "RA", new[] { new VisemeMapping("jawOpen", 0.111740f) } },
+ { "RU", new[] { new VisemeMapping("jawOpen", 0.110000f) } },
+
+ // Flap
+ { "FLAP", new[] { new VisemeMapping("lowerLipCurlOut", 0.120000f), new VisemeMapping("jawOpen", 0.020000f), new VisemeMapping("lowerLipCurlin", 0.130000f), new VisemeMapping("tongueUp", 0.660000f) } },
+
+ // Fricatives
+ { "PH", new[] { new VisemeMapping("jawOpen", 0.046000f), new VisemeMapping("upperLipCurlin", 1.000000f), new VisemeMapping("lowerLipCurlin", 1.000000f), new VisemeMapping("pucker", 1.000000f), new VisemeMapping("sneerRight", 0.150000f), new VisemeMapping("sneerLeft", 0.150000f) } },
+ { "F", new[] { new VisemeMapping("jawOpen", 0.050000f), new VisemeMapping("upperLipCurlin", 1.000000f), new VisemeMapping("lowerLipCurlin", 1.000000f), new VisemeMapping("pucker", 1.000000f), new VisemeMapping("tongueUp", 0.660000f), new VisemeMapping("sneerRight", 0.150000f), new VisemeMapping("sneerLeft", 0.150000f) } },
+ { "V", new[] { new VisemeMapping("jawOpen", 0.054300f), new VisemeMapping("upperLipCurlin", 0.590000f), new VisemeMapping("lowerLipCurlin", 1.000000f), new VisemeMapping("pucker", 0.530000f) } },
+ { "TH", new[] { new VisemeMapping("sneerRight", 0.290000f), new VisemeMapping("sneerLeft", 0.500000f), new VisemeMapping("tongueUp1", 0.230000f), new VisemeMapping("tongueUp2", 0.500000f), new VisemeMapping("tongueUp3", 0.800000f), new VisemeMapping("MandibleFlareRight", 0.130000f), new VisemeMapping("MandibleFlareLeft", 0.220000f) } },
+ { "DH", new[] { new VisemeMapping("tongueUp1", 0.350000f), new VisemeMapping("tongueUp2", 0.750000f), new VisemeMapping("tongueUp3", 0.730000f) } },
+ { "S", new[] { new VisemeMapping("jawOpen", 0.084000f), new VisemeMapping("smileRight", 1.000000f), new VisemeMapping("smileLeft", 1.000000f), new VisemeMapping("sneerRight", 0.210000f), new VisemeMapping("sneerLeft", 0.210000f) } },
+ { "Z", new[] { new VisemeMapping("jawOpen", 0.910000f), new VisemeMapping("smileRight", 1.000000f), new VisemeMapping("smileLeft", 1.000000f), new VisemeMapping("sneerRight", 0.260000f), new VisemeMapping("sneerLeft", 0.260000f) } },
+ { "SH", new[] { new VisemeMapping("upperLipCurlOut", 1.000000f), new VisemeMapping("lowerLipCurlOut", 1.000000f), new VisemeMapping("O_mouth", 1.000000f), new VisemeMapping("jawOpen", 0.039000f), new VisemeMapping("pucker", 0.530000f) } },
+ { "ZH", new[] { new VisemeMapping("upperLipCurlOut", 1.000000f), new VisemeMapping("lowerLipCurlOut", 1.000000f), new VisemeMapping("O_mouth", 1.000000f), new VisemeMapping("sneerRight", 0.250000f), new VisemeMapping("sneerLeft", 0.250000f), new VisemeMapping("tongueUp1", 0.680000f), new VisemeMapping("tongueUp2", 0.520000f), new VisemeMapping("tongueUp3", 0.210000f) } },
+ { "CX", new[] { new VisemeMapping("upperLipCurlOut", 0.350000f), new VisemeMapping("lowerLipCurlOut", 0.390000f), new VisemeMapping("O_mouth", 0.080000f), new VisemeMapping("sneerRight", 0.100000f), new VisemeMapping("sneerLeft", 0.100000f) } },
+ { "X", new[] { new VisemeMapping("upperLipCurlOut", 0.350000f), new VisemeMapping("lowerLipCurlOut", 0.390000f), new VisemeMapping("O_mouth", 0.080000f), new VisemeMapping("sneerRight", 0.100000f), new VisemeMapping("sneerLeft", 0.100000f) } },
+ { "GH", new[] { new VisemeMapping("upperLipCurlOut", 0.500000f), new VisemeMapping("lowerLipCurlOut", 0.520000f), new VisemeMapping("O_mouth", 0.160000f), new VisemeMapping("jawOpen", 0.010000f) } },
+ { "HH", new[] { new VisemeMapping("upperLipCurlOut", 0.370000f), new VisemeMapping("lowerLipCurlOut", 0.390000f), new VisemeMapping("O_mouth", 0.330000f), new VisemeMapping("pucker", 0.900000f), new VisemeMapping("noseDown", 0.200000f) } },
+ { "H", new[] { new VisemeMapping("upperLipCurlOut", 0.370000f), new VisemeMapping("lowerLipCurlOut", 0.390000f), new VisemeMapping("O_mouth", 0.330000f), new VisemeMapping("pucker", 0.900000f), new VisemeMapping("noseDown", 0.200000f) } },
+
+ // Approximants
+ { "R", new[] { new VisemeMapping("jawOpen", 0.110000f) } },
+ { "Y", new[] { new VisemeMapping("upperLipCurlOut", 0.370000f), new VisemeMapping("lowerLipCurlOut", 0.390000f), new VisemeMapping("O_mouth", 0.480000f), new VisemeMapping("jawOpen", 0.030000f), new VisemeMapping("pucker", 0.900000f), new VisemeMapping("noseDown", 0.200000f), new VisemeMapping("smileRight", 1.000000f), new VisemeMapping("smileLeft", 1.000000f) } },
+ { "L", new[] { new VisemeMapping("O_mouth", 0.260000f), new VisemeMapping("jawOpen", 0.070000f), new VisemeMapping("tongueUp1", 0.430000f), new VisemeMapping("tongueUp2", 0.630000f), new VisemeMapping("tongueUp3", 0.680000f), new VisemeMapping("mouthDownRight", 0.050000f), new VisemeMapping("jawClench", 0.440000f) } },
+ { "W", new[] { new VisemeMapping("lowerLipCurlOut", 0.010000f), new VisemeMapping("upperLipCurlin", 1.000000f), new VisemeMapping("lowerLipCurlin", 0.700000f), new VisemeMapping("pucker", 1.000000f), new VisemeMapping("noseDown", 0.100000f) } },
+
+ // Special
+ { "TS", new[] { new VisemeMapping("jawOpen", 0.030000f), new VisemeMapping("upperLipCurlin", 0.760000f), new VisemeMapping("lowerLipCurlin", 0.760000f), new VisemeMapping("pucker", 0.170000f), new VisemeMapping("noseDown", 0.210000f), new VisemeMapping("tongueUp2", 0.505517f), new VisemeMapping("tongueUp3", 0.810000f) } },
+
+ // Affricates
+ { "CH", new[] { new VisemeMapping("upperLipCurlOut", 1.000000f), new VisemeMapping("lowerLipCurlOut", 1.000000f), new VisemeMapping("O_mouth", 0.300000f) } },
+ { "JH", new[] { new VisemeMapping("upperLipCurlOut", 1.000000f), new VisemeMapping("lowerLipCurlOut", 1.000000f), new VisemeMapping("O_mouth", 0.220000f) } },
+
+ // Front vowels
+ { "IY", new[] { new VisemeMapping("jawOpen", 0.020000f), new VisemeMapping("smileRight", 1.000000f), new VisemeMapping("smileLeft", 1.000000f), new VisemeMapping("sneerRight", 0.300000f), new VisemeMapping("sneerLeft", 0.300000f) } },
+ { "IH", new[] { new VisemeMapping("jawOpen", 0.200000f), new VisemeMapping("smileRight", 1.000000f), new VisemeMapping("smileLeft", 1.000000f), new VisemeMapping("sneerRight", 0.250000f), new VisemeMapping("sneerLeft", 0.250000f), new VisemeMapping("jawForward", 0.380000f) } },
+ { "E", new[] { new VisemeMapping("O_mouth", 0.300000f), new VisemeMapping("jawOpen", 0.080000f), new VisemeMapping("sneerRight", 0.131000f), new VisemeMapping("sneerLeft", 0.151000f), new VisemeMapping("MandibleFlareRight", 0.053000f), new VisemeMapping("MandibleFlareLeft", 0.020000f) } },
+ { "EN", new[] { new VisemeMapping("O_mouth", 0.300000f), new VisemeMapping("jawOpen", 0.080000f), new VisemeMapping("smileRight", 1.000000f), new VisemeMapping("smileLeft", 1.000000f), new VisemeMapping("sneerRight", 0.130000f), new VisemeMapping("sneerLeft", 0.150000f), new VisemeMapping("MandibleFlareRight", 0.050000f), new VisemeMapping("MandibleFlareLeft", 0.020000f) } },
+ { "EH", new[] { new VisemeMapping("jawOpen", 0.120000f), new VisemeMapping("smileRight", 1.000000f), new VisemeMapping("smileLeft", 1.000000f) } },
+ { "EY", new[] { new VisemeMapping("jawOpen", 0.120000f), new VisemeMapping("smileRight", 1.000000f), new VisemeMapping("smileLeft", 1.000000f), new VisemeMapping("sneerRight", 1.000000f), new VisemeMapping("sneerLeft", 1.000000f), new VisemeMapping("MandibleFlareRight", 0.140000f), new VisemeMapping("MandibleFlareLeft", 0.099856f) } },
+ { "AE", new[] { new VisemeMapping("jawOpen", 0.130000f) } },
+
+ // Central vowels
+ { "AH", new[] { new VisemeMapping("jawOpen", 0.300000f), new VisemeMapping("MandibleFlareRight", 0.050000f), new VisemeMapping("MandibleFlareLeft", 0.050000f), new VisemeMapping("noseUp", 0.040000f) } },
+ { "AX", new[] { new VisemeMapping("jawOpen", 0.121000f), new VisemeMapping("smileRight", 0.710000f), new VisemeMapping("smileLeft", 0.710000f) } },
+ { "UX", new[] { new VisemeMapping("jawOpen", 0.120000f), new VisemeMapping("smileRight", 0.710000f), new VisemeMapping("smileLeft", 0.710000f) } },
+ { "ER", new[] { new VisemeMapping("jawOpen", 0.100000f), new VisemeMapping("lowerLipCurlin", 1.000000f), new VisemeMapping("smileRight", 1.000000f), new VisemeMapping("smileLeft", 1.000000f), new VisemeMapping("tongueUp1", 0.730000f), new VisemeMapping("tongueUp2", 0.796000f), new VisemeMapping("tongueUp3", 0.150000f) } },
+ { "AXR", new[] { new VisemeMapping("jawOpen", 0.100000f), new VisemeMapping("lowerLipCurlin", 1.000000f), new VisemeMapping("smileRight", 1.000000f), new VisemeMapping("smileLeft", 1.000000f), new VisemeMapping("tongueUp1", 0.730000f), new VisemeMapping("tongueUp2", 0.800000f), new VisemeMapping("tongueUp3", 0.150000f) } },
+ { "EXR", new[] { new VisemeMapping("jawOpen", 0.100000f), new VisemeMapping("lowerLipCurlin", 1.000000f), new VisemeMapping("smileRight", 1.000000f), new VisemeMapping("smileLeft", 1.000000f), new VisemeMapping("tongueUp1", 0.730000f), new VisemeMapping("tongueUp2", 0.800000f), new VisemeMapping("tongueUp3", 0.150000f) } },
+
+ // Back vowels
+ { "A", new[] { new VisemeMapping("O_mouth", 0.300000f), new VisemeMapping("jawOpen", 0.080000f), new VisemeMapping("sneerRight", 0.130000f), new VisemeMapping("sneerLeft", 0.150000f), new VisemeMapping("MandibleFlareRight", 0.050000f), new VisemeMapping("MandibleFlareLeft", 0.020000f) } },
+ { "AA", new[] { new VisemeMapping("O_mouth", 0.300000f), new VisemeMapping("jawOpen", 0.200000f), new VisemeMapping("sneerRight", 0.130000f), new VisemeMapping("sneerLeft", 0.150000f), new VisemeMapping("MandibleFlareRight", 0.050000f), new VisemeMapping("MandibleFlareLeft", 0.020000f) } },
+ { "AAN", new[] { new VisemeMapping("O_mouth", 0.300000f), new VisemeMapping("jawOpen", 0.080000f), new VisemeMapping("sneerRight", 0.130000f), new VisemeMapping("sneerLeft", 0.150000f), new VisemeMapping("MandibleFlareRight", 0.050000f), new VisemeMapping("MandibleFlareLeft", 0.020000f) } },
+ { "AO", new[] { new VisemeMapping("lowerLipCurlOut", 1.000000f), new VisemeMapping("O_mouth", 0.380000f), new VisemeMapping("jawOpen", 0.170000f) } },
+ { "AON", new[] { new VisemeMapping("lowerLipCurlOut", 1.000000f), new VisemeMapping("O_mouth", 0.380000f), new VisemeMapping("jawOpen", 0.170000f) } },
+ { "O", new[] { new VisemeMapping("lowerLipCurlOut", 1.000000f), new VisemeMapping("O_mouth", 0.380000f), new VisemeMapping("jawOpen", 0.250000f) } },
+ { "ON", new[] { new VisemeMapping("lowerLipCurlOut", 1.000000f), new VisemeMapping("O_mouth", 0.380000f), new VisemeMapping("jawOpen", 0.170000f) } },
+ { "UW", new[] { new VisemeMapping("upperLipCurlOut", 1.000000f), new VisemeMapping("lowerLipCurlOut", 1.000000f), new VisemeMapping("O_mouth", 1.000000f), new VisemeMapping("jawOpen", 0.170000f), new VisemeMapping("jawForward", 0.470000f) } },
+ { "UH", new[] { new VisemeMapping("upperLipCurlOut", 0.870000f), new VisemeMapping("lowerLipCurlOut", 0.850000f), new VisemeMapping("O_mouth", 0.390000f), new VisemeMapping("jawOpen", 0.100000f), new VisemeMapping("pucker", 1.000000f) } },
+ { "OW", new[] { new VisemeMapping("upperLipCurlOut", 1.000000f), new VisemeMapping("lowerLipCurlOut", 1.000000f), new VisemeMapping("O_mouth", 1.000000f), new VisemeMapping("jawOpen", 0.140000f), new VisemeMapping("tongueUp", 0.337707f), new VisemeMapping("smileRight", 0.470000f), new VisemeMapping("smileLeft", 0.170000f) } },
+ { "UY", new[] { new VisemeMapping("lowerLipCurlOut", 0.010000f), new VisemeMapping("upperLipCurlin", 1.000000f), new VisemeMapping("lowerLipCurlin", 0.710000f), new VisemeMapping("pucker", 1.000000f), new VisemeMapping("noseDown", 0.100000f) } },
+ { "UU", new[] { new VisemeMapping("lowerLipCurlOut", 0.005952f), new VisemeMapping("upperLipCurlin", 1.000000f), new VisemeMapping("lowerLipCurlin", 0.710000f), new VisemeMapping("pucker", 1.000000f), new VisemeMapping("noseDown", 0.100000f) } },
+ { "EU", new[] { new VisemeMapping("lowerLipCurlOut", 1.000000f), new VisemeMapping("O_mouth", 0.380000f), new VisemeMapping("jawOpen", 0.170000f) } },
+ { "OE", new[] { new VisemeMapping("lowerLipCurlOut", 1.000000f), new VisemeMapping("O_mouth", 0.380000f), new VisemeMapping("jawOpen", 0.170000f) } },
+ { "OEN", new[] { new VisemeMapping("lowerLipCurlOut", 1.000000f), new VisemeMapping("O_mouth", 0.380000f), new VisemeMapping("jawOpen", 0.170000f) } },
+
+ // Diphthongs
+ { "AY", new[] { new VisemeMapping("jawOpen", 0.120000f), new VisemeMapping("smileRight", 1.000000f), new VisemeMapping("smileLeft", 1.000000f), new VisemeMapping("sneerRight", 0.150000f), new VisemeMapping("sneerLeft", 0.150000f) } },
+ { "AW", new[] { new VisemeMapping("upperLipCurlOut", 0.450000f), new VisemeMapping("lowerLipCurlOut", 0.510000f), new VisemeMapping("O_mouth", 1.000000f), new VisemeMapping("jawOpen", 0.080357f), new VisemeMapping("pucker", 1.000000f) } },
+ { "OY", new[] { new VisemeMapping("upperLipCurlOut", 1.000000f), new VisemeMapping("lowerLipCurlOut", 1.000000f), new VisemeMapping("O_mouth", 1.000000f), new VisemeMapping("jawOpen", 0.100000f) } },
+ };
+
+ ///
+ /// All viseme animation names used in lip sync for Turian
+ ///
+ public static readonly string[] TurianVisemes =
+ [
+ "jawClench",
+ "lowerLipCurlin",
+ "pucker",
+ "upperLipCurlin",
+ "noseDown",
+ "jawOpen",
+ "tongueUp",
+ "tongueUp1",
+ "tongueUp2",
+ "tongueUp3",
+ "upperLipCurlOut",
+ "lowerLipCurlOut",
+ "sneerRight",
+ "sneerLeft",
+ "O_mouth",
+ "smileRight",
+ "smileLeft",
+ "MandibleFlareRight",
+ "MandibleFlareLeft",
+ "mouthDownRight",
+ "jawForward",
+ "noseUp"
+ ];
+
+ ///
+ /// Salarian phoneme to viseme mappings - from SAL_HED_PROBASE_MDL_FaceFX data.
+ ///
+ public static readonly Dictionary SalarianPhonemeMap = new()
+ {
+ // Alveolar stops
+ { "T", new[] { new VisemeMapping("smileRight", 0.040000f), new VisemeMapping("smileLeft", 0.040000f), new VisemeMapping("sneerRight", 0.100000f), new VisemeMapping("sneerLeft", 0.120000f), new VisemeMapping("frownRight", 0.200000f), new VisemeMapping("frownLeft", 0.200000f), new VisemeMapping("jawForward", 0.350000f) } },
+ { "D", new[] { new VisemeMapping("sneerLeft", 0.100000f), new VisemeMapping("sneerRight", 0.110000f), new VisemeMapping("frownRight", 0.300000f), new VisemeMapping("frownLeft", 0.300000f), new VisemeMapping("jawRotate", 0.060000f), new VisemeMapping("jawOpen", 0.040000f), new VisemeMapping("mouthDownLeft", 0.210000f), new VisemeMapping("tongueUP", 1.000000f), new VisemeMapping("mouthDownRight", 0.200000f) } },
+
+ // Velar stops
+ { "K", new[] { new VisemeMapping("frownRight", 0.200000f), new VisemeMapping("frownLeft", 0.200000f), new VisemeMapping("jawOpen", 0.050000f) } },
+ { "G", new[] { new VisemeMapping("sneerRight", 0.113839f), new VisemeMapping("sneerLeft", 0.087798f), new VisemeMapping("frownRight", 0.348214f), new VisemeMapping("frownLeft", 0.340774f), new VisemeMapping("jawRotate", 0.220000f), new VisemeMapping("pucker", 0.130000f), new VisemeMapping("upperLipCurlIn", 0.330000f), new VisemeMapping("lowerLipCurlIn", 0.046875f), new VisemeMapping("jawRotateUp", 0.229167f) } },
+
+ // Bilabial stops
+ { "P", new[] { new VisemeMapping("pucker", 0.050000f), new VisemeMapping("upperLipCurlIn", 0.330000f), new VisemeMapping("jawClench", 0.240000f), new VisemeMapping("lowerLipCurlIn", 0.420000f) } },
+ { "B", new[] { new VisemeMapping("upperLipCurlIn", 0.110000f), new VisemeMapping("lowerLipCurlIn", 0.110000f), new VisemeMapping("jawClench", 0.190000f) } },
+ { "M", new[] { new VisemeMapping("upperLipCurlIn", 0.690000f), new VisemeMapping("lowerLipCurlIn", 0.890000f) } },
+
+ // Nasals
+ { "N", new[] { new VisemeMapping("sneerRight", 0.113957f), new VisemeMapping("sneerLeft", 0.118523f), new VisemeMapping("jawRotateUp", 0.220107f), new VisemeMapping("tongueUP", 1.000000f), new VisemeMapping("jawRotate", 0.030000f), new VisemeMapping("O_mouth", 0.100000f), new VisemeMapping("jawOpen", 0.070000f) } },
+ { "NG", new[] { new VisemeMapping("frownRight", 0.200000f), new VisemeMapping("frownLeft", 0.200000f), new VisemeMapping("jawOpen", 0.060000f), new VisemeMapping("lowerLipCurlIn", 0.260000f) } },
+
+ // R variants
+ { "RA", new[] { new VisemeMapping("jawRotate", 0.110000f), new VisemeMapping("jawOpen", 0.040000f), new VisemeMapping("O_mouth", 0.200000f) } },
+ { "RU", new[] { new VisemeMapping("jawRotate", 0.110000f), new VisemeMapping("jawOpen", 0.040000f), new VisemeMapping("O_mouth", 0.200000f) } },
+
+ // Flap
+ { "FLAP", new[] { new VisemeMapping("jawOpen", 0.005000f) } },
+
+ // Fricatives
+ { "PH", new[] { new VisemeMapping("sneerRight", 0.070000f), new VisemeMapping("sneerLeft", 0.060000f), new VisemeMapping("upperLipCurlOut", 0.520000f), new VisemeMapping("lowerLipCurlIn", 1.000000f) } },
+ { "F", new[] { new VisemeMapping("sneerRight", 0.070000f), new VisemeMapping("sneerLeft", 0.060000f), new VisemeMapping("upperLipCurlOut", 0.520000f), new VisemeMapping("lowerLipCurlIn", 1.000000f) } },
+ { "V", new[] { new VisemeMapping("sneerRight", 0.060000f), new VisemeMapping("sneerLeft", 0.040000f), new VisemeMapping("lowerLipCurlIn", 1.000000f), new VisemeMapping("upperLipCurlOut", 0.490000f), new VisemeMapping("jawClench", 0.170000f) } },
+ { "TH", new[] { new VisemeMapping("sneerRight", 0.173310f), new VisemeMapping("sneerLeft", 0.164179f), new VisemeMapping("jawRotate", 0.053463f), new VisemeMapping("O_mouth", 0.200000f), new VisemeMapping("jawOpen", 0.039766f), new VisemeMapping("jawRotateUp", 0.210976f), new VisemeMapping("tongueUP", 1.000000f) } },
+ { "DH", new[] { new VisemeMapping("jawRotate", 0.070000f), new VisemeMapping("O_mouth", 0.100000f), new VisemeMapping("jawOpen", 0.060000f), new VisemeMapping("upperLipCurlOut", 0.430000f), new VisemeMapping("tongueUP", 1.000000f), new VisemeMapping("jawRotateUp", 0.119664f) } },
+ { "S", new[] { new VisemeMapping("smileRight", 0.050000f), new VisemeMapping("smileLeft", 0.050000f), new VisemeMapping("sneerRight", 0.180000f), new VisemeMapping("sneerLeft", 0.210000f), new VisemeMapping("frownLeft", 0.300000f), new VisemeMapping("frownRight", 0.300000f), new VisemeMapping("jawForward", 0.020000f) } },
+ { "Z", new[] { new VisemeMapping("smileRight", 0.060000f), new VisemeMapping("smileLeft", 0.060000f), new VisemeMapping("jawForward", 0.220000f), new VisemeMapping("sneerRight", 0.130000f), new VisemeMapping("sneerLeft", 0.150000f), new VisemeMapping("frownRight", 0.200000f), new VisemeMapping("frownLeft", 0.200000f) } },
+ { "SH", new[] { new VisemeMapping("sneerRight", 0.120000f), new VisemeMapping("sneerLeft", 0.100000f), new VisemeMapping("frownLeft", 0.100000f), new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("O_mouth", 0.300000f) } },
+ { "ZH", new[] { new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.100000f), new VisemeMapping("sneerRight", 0.070000f), new VisemeMapping("sneerLeft", 0.070000f), new VisemeMapping("O_mouth", 0.200000f), new VisemeMapping("pucker", 0.050000f), new VisemeMapping("lowerLipCurlOut", 0.300000f), new VisemeMapping("jawForward", 0.340000f) } },
+ { "X", new[] { new VisemeMapping("frownRight", 0.200000f), new VisemeMapping("frownLeft", 0.200000f), new VisemeMapping("jawOpen", 0.050000f) } },
+ { "CX", new[] { new VisemeMapping("frownRight", 0.200000f), new VisemeMapping("frownLeft", 0.200000f), new VisemeMapping("jawOpen", 0.050000f) } },
+ { "GH", new[] { new VisemeMapping("sneerRight", 0.113839f), new VisemeMapping("sneerLeft", 0.087798f), new VisemeMapping("frownRight", 0.348214f), new VisemeMapping("frownLeft", 0.340774f), new VisemeMapping("jawRotate", 0.220000f), new VisemeMapping("pucker", 0.130000f), new VisemeMapping("upperLipCurlIn", 0.330000f), new VisemeMapping("lowerLipCurlIn", 0.046875f), new VisemeMapping("jawRotateUp", 0.229167f) } },
+ { "HH", new[] { new VisemeMapping("frownRight", 0.300000f), new VisemeMapping("frownLeft", 0.300000f), new VisemeMapping("jawRotate", 0.190000f), new VisemeMapping("pucker", 0.050000f) } },
+ { "H", new[] { new VisemeMapping("frownRight", 0.300000f), new VisemeMapping("frownLeft", 0.300000f), new VisemeMapping("jawRotate", 0.190000f), new VisemeMapping("pucker", 0.050000f) } },
+
+ // Approximants
+ { "R", new[] { new VisemeMapping("jawRotate", 0.110000f), new VisemeMapping("O_mouth", 0.200000f), new VisemeMapping("jawOpen", 0.040000f) } },
+ { "Y", new[] { new VisemeMapping("frownRight", 0.300000f), new VisemeMapping("frownLeft", 0.300000f), new VisemeMapping("jawRotate", 0.100000f) } },
+ { "L", new[] { new VisemeMapping("frownRight", 0.200000f), new VisemeMapping("frownLeft", 0.200000f), new VisemeMapping("jawRotate", 0.060000f), new VisemeMapping("jawOpen", 0.040000f), new VisemeMapping("tongueUP", 1.000000f) } },
+ { "W", new[] { new VisemeMapping("pucker", 0.300000f) } },
+
+ // Special
+ { "TS", new[] { new VisemeMapping("smileRight", 0.040000f), new VisemeMapping("smileLeft", 0.040000f), new VisemeMapping("sneerRight", 0.100000f), new VisemeMapping("sneerLeft", 0.120000f), new VisemeMapping("frownLeft", 0.200000f), new VisemeMapping("frownRight", 0.200000f), new VisemeMapping("jawForward", 0.350000f) } },
+
+ // Affricates
+ { "CH", new[] { new VisemeMapping("sneerRight", 0.090000f), new VisemeMapping("sneerLeft", 0.090000f), new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.070000f), new VisemeMapping("O_mouth", 0.100000f), new VisemeMapping("pucker", 0.050000f) } },
+ { "JH", new[] { new VisemeMapping("sneerRight", 0.130000f), new VisemeMapping("sneerLeft", 0.100000f), new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.100000f), new VisemeMapping("O_mouth", 0.100000f), new VisemeMapping("lowerLipCurlOut", 0.160000f), new VisemeMapping("jawForward", 0.340000f), new VisemeMapping("mouthDownLeft", 0.070000f), new VisemeMapping("mouthDownRight", 0.060000f) } },
+
+ // Front vowels
+ { "IY", new[] { new VisemeMapping("smileRight", 0.118176f), new VisemeMapping("smileLeft", 0.104346f), new VisemeMapping("sneerRight", 0.080000f), new VisemeMapping("frownRight", 0.300000f), new VisemeMapping("frownLeft", 0.300000f), new VisemeMapping("sneerLeft", 0.090000f), new VisemeMapping("jawRotate", 0.160000f), new VisemeMapping("jawOpen", 0.020000f), new VisemeMapping("mouthDownLeft", 0.210000f), new VisemeMapping("mouthDownRight", 0.220000f) } },
+ { "IH", new[] { new VisemeMapping("smileRight", 0.040000f), new VisemeMapping("smileLeft", 0.040000f), new VisemeMapping("frownRight", 0.300000f), new VisemeMapping("frownLeft", 0.300000f), new VisemeMapping("jawOpen", 0.030000f), new VisemeMapping("jawRotate", 0.200000f), new VisemeMapping("mouthDownLeft", 0.230000f), new VisemeMapping("mouthDownRight", 0.210000f) } },
+ { "EH", new[] { new VisemeMapping("smileRight", 0.070000f), new VisemeMapping("smileLeft", 0.070000f), new VisemeMapping("sneerRight", 0.120000f), new VisemeMapping("sneerLeft", 0.110000f), new VisemeMapping("frownRight", 0.200000f), new VisemeMapping("frownLeft", 0.200000f), new VisemeMapping("jawRotate", 0.250000f) } },
+ { "EY", new[] { new VisemeMapping("jawRotate", 0.160000f), new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.100000f), new VisemeMapping("jawOpen", 0.070000f), new VisemeMapping("mouthDownLeft", 0.220000f), new VisemeMapping("mouthDownRight", 0.210000f), new VisemeMapping("smileRight", 0.081651f), new VisemeMapping("smileLeft", 0.131740f) } },
+ { "AE", new[] { new VisemeMapping("jawRotate", 0.060000f), new VisemeMapping("jawOpen", 0.080000f), new VisemeMapping("lowerLipCurlOut", 0.270000f), new VisemeMapping("mouthDownLeft", 0.180000f), new VisemeMapping("mouthDownRight", 0.190000f) } },
+ { "E", new[] { new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.100000f), new VisemeMapping("jawRotate", 0.060000f), new VisemeMapping("jawOpen", 0.089988f), new VisemeMapping("mouthDownLeft", 0.170000f), new VisemeMapping("mouthDownRight", 0.140000f) } },
+ { "EN", new[] { new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.100000f), new VisemeMapping("jawRotate", 0.060000f), new VisemeMapping("jawOpen", 0.089988f), new VisemeMapping("mouthDownLeft", 0.170000f), new VisemeMapping("mouthDownRight", 0.140000f) } },
+
+ // Central vowels
+ { "AH", new[] { new VisemeMapping("jawRotate", 0.190000f), new VisemeMapping("O_mouth", 0.100000f), new VisemeMapping("jawOpen", 0.090000f), new VisemeMapping("mouthDownLeft", 0.160000f), new VisemeMapping("mouthDownRight", 0.150000f) } },
+ { "AX", new[] { new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.100000f), new VisemeMapping("jawRotate", 0.100000f), new VisemeMapping("jawOpen", 0.060000f) } },
+ { "UX", new[] { new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.100000f), new VisemeMapping("jawRotate", 0.100000f), new VisemeMapping("jawOpen", 0.060000f) } },
+ { "ER", new[] { new VisemeMapping("smileLeft", 0.040000f), new VisemeMapping("frownRight", 0.200000f), new VisemeMapping("frownLeft", 0.200000f), new VisemeMapping("jawRotate", 0.160000f), new VisemeMapping("jawOpen", 0.040000f), new VisemeMapping("upperLipCurlIn", 0.190000f), new VisemeMapping("jawClench", 0.280000f) } },
+ { "AXR", new[] { new VisemeMapping("smileLeft", 0.040000f), new VisemeMapping("frownRight", 0.200000f), new VisemeMapping("frownLeft", 0.200000f), new VisemeMapping("jawRotate", 0.160000f), new VisemeMapping("jawOpen", 0.040000f), new VisemeMapping("upperLipCurlIn", 0.190000f), new VisemeMapping("jawClench", 0.280000f) } },
+ { "EXR", new[] { new VisemeMapping("smileLeft", 0.040000f), new VisemeMapping("frownRight", 0.200000f), new VisemeMapping("frownLeft", 0.200000f), new VisemeMapping("jawRotate", 0.160000f), new VisemeMapping("jawOpen", 0.040000f), new VisemeMapping("upperLipCurlIn", 0.190000f), new VisemeMapping("jawClench", 0.280000f) } },
+
+ // Back vowels
+ { "UW", new[] { new VisemeMapping("jawRotate", 0.140000f), new VisemeMapping("jawOpen", 0.050000f), new VisemeMapping("O_mouth", 0.363923f), new VisemeMapping("pucker", 0.099119f), new VisemeMapping("lowerLipCurlOut", 0.570000f) } },
+ { "UH", new[] { new VisemeMapping("jawRotate", 0.150000f), new VisemeMapping("jawOpen", 0.040000f), new VisemeMapping("O_mouth", 0.200000f), new VisemeMapping("upperLipCurlOut", 0.220000f), new VisemeMapping("lowerLipCurlOut", 0.510000f) } },
+ { "OW", new[] { new VisemeMapping("jawRotate", 0.190000f), new VisemeMapping("O_mouth", 0.200000f), new VisemeMapping("jawOpen", 0.060000f), new VisemeMapping("pucker", 0.050000f) } },
+ { "AA", new[] { new VisemeMapping("jawOpen", 0.089988f), new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.100000f), new VisemeMapping("jawRotate", 0.060000f), new VisemeMapping("mouthDownLeft", 0.170000f), new VisemeMapping("mouthDownRight", 0.140000f) } },
+ { "A", new[] { new VisemeMapping("jawOpen", 0.089988f), new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.100000f), new VisemeMapping("jawRotate", 0.060000f), new VisemeMapping("mouthDownLeft", 0.170000f), new VisemeMapping("mouthDownRight", 0.140000f) } },
+ { "AAN", new[] { new VisemeMapping("jawOpen", 0.089988f), new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.100000f), new VisemeMapping("jawRotate", 0.060000f), new VisemeMapping("mouthDownLeft", 0.170000f), new VisemeMapping("mouthDownRight", 0.140000f) } },
+ { "AO", new[] { new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.100000f), new VisemeMapping("jawRotate", 0.140000f), new VisemeMapping("jawOpen", 0.080000f), new VisemeMapping("mouthDownLeft", 0.170000f), new VisemeMapping("mouthDownRight", 0.140000f), new VisemeMapping("pucker", 0.050000f), new VisemeMapping("lowerLipCurlOut", 0.180000f) } },
+ { "AON", new[] { new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.100000f), new VisemeMapping("jawRotate", 0.140000f), new VisemeMapping("jawOpen", 0.080000f), new VisemeMapping("mouthDownLeft", 0.170000f), new VisemeMapping("mouthDownRight", 0.140000f), new VisemeMapping("pucker", 0.050000f), new VisemeMapping("lowerLipCurlOut", 0.180000f) } },
+ { "O", new[] { new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.100000f), new VisemeMapping("jawRotate", 0.140000f), new VisemeMapping("jawOpen", 0.080000f), new VisemeMapping("mouthDownLeft", 0.170000f), new VisemeMapping("mouthDownRight", 0.140000f), new VisemeMapping("pucker", 0.050000f), new VisemeMapping("lowerLipCurlOut", 0.180000f) } },
+ { "ON", new[] { new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.100000f), new VisemeMapping("jawRotate", 0.140000f), new VisemeMapping("jawOpen", 0.080000f), new VisemeMapping("mouthDownLeft", 0.170000f), new VisemeMapping("mouthDownRight", 0.140000f), new VisemeMapping("pucker", 0.050000f), new VisemeMapping("lowerLipCurlOut", 0.180000f) } },
+ { "UY", new[] { new VisemeMapping("pucker", 0.300000f) } },
+ { "UU", new[] { new VisemeMapping("pucker", 0.300000f) } },
+ { "EU", new[] { new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.100000f), new VisemeMapping("jawRotate", 0.140000f), new VisemeMapping("jawOpen", 0.080000f), new VisemeMapping("pucker", 0.050000f), new VisemeMapping("lowerLipCurlOut", 0.180000f), new VisemeMapping("mouthDownLeft", 0.170000f), new VisemeMapping("mouthDownRight", 0.140000f) } },
+ { "OE", new[] { new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.100000f), new VisemeMapping("jawRotate", 0.140000f), new VisemeMapping("jawOpen", 0.080000f), new VisemeMapping("pucker", 0.050000f), new VisemeMapping("lowerLipCurlOut", 0.180000f), new VisemeMapping("mouthDownLeft", 0.170000f), new VisemeMapping("mouthDownRight", 0.140000f) } },
+ { "OEN", new[] { new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.100000f), new VisemeMapping("jawRotate", 0.140000f), new VisemeMapping("jawOpen", 0.080000f), new VisemeMapping("pucker", 0.050000f), new VisemeMapping("lowerLipCurlOut", 0.180000f), new VisemeMapping("mouthDownLeft", 0.170000f), new VisemeMapping("mouthDownRight", 0.140000f) } },
+
+ // Diphthongs
+ { "AY", new[] { new VisemeMapping("frownRight", 0.200000f), new VisemeMapping("frownLeft", 0.200000f), new VisemeMapping("jawOpen", 0.010000f), new VisemeMapping("jawRotate", 0.160000f), new VisemeMapping("mouthDownLeft", 0.230000f), new VisemeMapping("mouthDownRight", 0.220000f) } },
+ { "AW", new[] { new VisemeMapping("jawRotate", 0.240000f), new VisemeMapping("O_mouth", 0.300000f), new VisemeMapping("jawOpen", 0.040000f), new VisemeMapping("mouthDownLeft", 0.140000f), new VisemeMapping("mouthDownRight", 0.120000f) } },
+ { "OY", new[] { new VisemeMapping("O_mouth", 0.300000f), new VisemeMapping("jawOpen", 0.080000f), new VisemeMapping("jawRotate", 0.210000f) } },
+ };
+
+ ///
+ /// All viseme animation names used in lip sync for Salarian
+ ///
+ public static readonly string[] SalarianVisemes =
+ [
+ "smileRight",
+ "smileLeft",
+ "sneerRight",
+ "sneerLeft",
+ "frownRight",
+ "frownLeft",
+ "jawForward",
+ "jawRotate",
+ "jawOpen",
+ "mouthDownLeft",
+ "mouthDownRight",
+ "tongueUP",
+ "pucker",
+ "upperLipCurlIn",
+ "lowerLipCurlIn",
+ "jawClench",
+ "jawRotateUp",
+ "O_mouth",
+ "upperLipCurlOut",
+ "lowerLipCurlOut"
+ ];
+
+ ///
+ /// Elcor phoneme to viseme mappings - from SFX_Elcor_FaceFX data.
+ /// Note: Elcor use bone names as phoneme identifiers in FaceFX mapping.
+ ///
+ public static readonly Dictionary ElcorPhonemeMap = new()
+ {
+ // Bone-based phoneme mappings from SFX_Elcor_FaceFX
+ { "brow_left", new[] { new VisemeMapping("sneerRight", 0.250000f), new VisemeMapping("sneerLeft", 0.250000f), new VisemeMapping("O_mouth", 1.000000f), new VisemeMapping("upperLipCurlOut", 1.000000f), new VisemeMapping("lowerLipCurlOut", 1.000000f) } },
+ { "brow_right", new[] { new VisemeMapping("smileRight", 1.000000f), new VisemeMapping("smileLeft", 1.000000f), new VisemeMapping("sneerRight", 0.210000f), new VisemeMapping("sneerLeft", 0.210000f), new VisemeMapping("jawOpen", 0.084077f) } },
+ { "cheek_left", new[] { new VisemeMapping("upperLipCurlOut", 0.500000f), new VisemeMapping("lowerLipCurlOut", 0.520000f), new VisemeMapping("jawOpen", 0.010000f), new VisemeMapping("O_mouth", 0.160000f) } },
+ { "cheek_right", new[] { new VisemeMapping("pucker", 0.534226f), new VisemeMapping("O_mouth", 1.000000f), new VisemeMapping("jawOpen", 0.039435f), new VisemeMapping("upperLipCurlOut", 1.000000f), new VisemeMapping("lowerLipCurlOut", 1.000000f) } },
+ { "Chest", new[] { new VisemeMapping("sneerRight", 0.130000f), new VisemeMapping("sneerLeft", 0.130000f), new VisemeMapping("upperLipCurlOut", 0.370000f), new VisemeMapping("lowerLipCurlOut", 0.230000f), new VisemeMapping("jawOpen", 0.050000f), new VisemeMapping("tongueUp1", 0.390000f), new VisemeMapping("tongueUp2", 0.660000f), new VisemeMapping("tongueUp3", 0.760000f) } },
+ { "Chest1", new[] { new VisemeMapping("sneerRight", 0.100000f), new VisemeMapping("sneerLeft", 0.100000f), new VisemeMapping("upperLipCurlOut", 0.350000f), new VisemeMapping("lowerLipCurlOut", 0.390000f), new VisemeMapping("O_mouth", 0.080000f) } },
+ { "eyeBlink_Left", new[] { new VisemeMapping("tongueUp1", 0.180000f), new VisemeMapping("tongueUp2", 0.270000f), new VisemeMapping("tongueUp3", 0.360000f), new VisemeMapping("tongueForward", 0.890000f) } },
+ { "eyeBlink_Right", new[] { new VisemeMapping("sneerRight", 0.100000f), new VisemeMapping("sneerLeft", 0.100000f), new VisemeMapping("upperLipCurlOut", 0.350000f), new VisemeMapping("lowerLipCurlOut", 0.390000f), new VisemeMapping("O_mouth", 0.080000f) } },
+ { "GOD", new[] { new VisemeMapping("upperLipCurlIn", 0.760000f), new VisemeMapping("lowerLipCurlIn", 0.760000f), new VisemeMapping("pucker", 0.170000f), new VisemeMapping("noseDown", 0.200000f) } },
+ { "Head", new[] { new VisemeMapping("smileRight", 0.430000f), new VisemeMapping("smileLeft", 0.430000f), new VisemeMapping("pucker", 1.000000f), new VisemeMapping("upperLipCurlIn", 1.000000f), new VisemeMapping("lowerLipCurlIn", 1.000000f) } },
+ { "jawBone", new[] { new VisemeMapping("sneerRight", 0.250000f), new VisemeMapping("sneerLeft", 0.250000f), new VisemeMapping("tongueUp1", 0.510000f), new VisemeMapping("tongueUp2", 0.530000f), new VisemeMapping("tongueUp3", 0.700000f) } },
+ { "LeftAnkle", new[] { new VisemeMapping("pucker", 1.000000f), new VisemeMapping("O_mouth", 1.000000f), new VisemeMapping("jawOpen", 0.080357f), new VisemeMapping("upperLipCurlOut", 0.450000f), new VisemeMapping("lowerLipCurlOut", 0.510000f) } },
+ { "LeftBangle", new[] { new VisemeMapping("jawOpen", 0.010000f) } },
+ { "LeftCollar", new[] { new VisemeMapping("upperLipCurlOut", 0.370000f), new VisemeMapping("lowerLipCurlOut", 0.390000f), new VisemeMapping("pucker", 0.900000f), new VisemeMapping("O_mouth", 0.330000f), new VisemeMapping("noseDown", 0.200000f) } },
+ { "LeftDigit", new[] { new VisemeMapping("O_mouth", 0.300000f), new VisemeMapping("jawOpen", 0.080357f) } },
+ { "LeftElbow", new[] { new VisemeMapping("upperLipCurlOut", 1.000000f), new VisemeMapping("lowerLipCurlOut", 1.000000f), new VisemeMapping("O_mouth", 0.300000f) } },
+ { "LeftFlap", new[] { new VisemeMapping("upperLipCurlOut", 0.370000f), new VisemeMapping("lowerLipCurlOut", 0.390000f), new VisemeMapping("pucker", 0.900000f), new VisemeMapping("O_mouth", 0.330000f), new VisemeMapping("noseDown", 0.200000f) } },
+ { "LeftFoot", new[] { new VisemeMapping("smileRight", 1.000000f), new VisemeMapping("smileLeft", 1.000000f), new VisemeMapping("sneerRight", 0.150000f), new VisemeMapping("sneerLeft", 0.150000f), new VisemeMapping("jawOpen", 0.120000f) } },
+ { "LeftHip", new[] { new VisemeMapping("smileRight", 1.000000f), new VisemeMapping("smileLeft", 1.000000f), new VisemeMapping("jawOpen", 0.098958f), new VisemeMapping("lowerLipCurlIn", 1.000000f) } },
+ { "LeftIndexFinger", new[] { new VisemeMapping("O_mouth", 0.300000f), new VisemeMapping("jawOpen", 0.080357f) } },
+ { "LeftIndexFinger1", new[] { new VisemeMapping("O_mouth", 0.380000f), new VisemeMapping("jawOpen", 0.173363f), new VisemeMapping("lowerLipCurlOut", 1.000000f) } },
+ { "LeftIndexFinger2", new[] { new VisemeMapping("O_mouth", 0.380000f), new VisemeMapping("jawOpen", 0.173363f), new VisemeMapping("lowerLipCurlOut", 1.000000f) } },
+ { "LeftIndexToe", new[] { new VisemeMapping("O_mouth", 1.000000f), new VisemeMapping("jawOpen", 0.136161f), new VisemeMapping("upperLipCurlOut", 1.000000f), new VisemeMapping("lowerLipCurlOut", 1.000000f) } },
+ { "LeftKnee", new[] { new VisemeMapping("smileRight", 1.000000f), new VisemeMapping("smileLeft", 1.000000f), new VisemeMapping("sneerRight", 0.150000f), new VisemeMapping("sneerLeft", 0.150000f), new VisemeMapping("jawOpen", 0.121280f) } },
+ { "LeftPinkFinger", new[] { new VisemeMapping("smileRight", 1.000000f), new VisemeMapping("smileLeft", 1.000000f), new VisemeMapping("jawOpen", 0.125000f) } },
+ { "LeftPinkFinger1", new[] { new VisemeMapping("O_mouth", 0.300000f), new VisemeMapping("jawOpen", 0.080357f) } },
+ { "LeftPinkFinger2", new[] { new VisemeMapping("O_mouth", 0.300000f), new VisemeMapping("jawOpen", 0.080357f) } },
+ { "LeftPinkToe", new[] { new VisemeMapping("O_mouth", 1.000000f), new VisemeMapping("jawOpen", 0.098958f), new VisemeMapping("upperLipCurlOut", 1.000000f), new VisemeMapping("lowerLipCurlOut", 1.000000f) } },
+ { "LeftShoulder", new[] { new VisemeMapping("upperLipCurlIn", 0.760000f), new VisemeMapping("lowerLipCurlIn", 0.760000f), new VisemeMapping("jawOpen", 0.030000f), new VisemeMapping("pucker", 0.170000f), new VisemeMapping("tongueUp1", 0.300000f), new VisemeMapping("tongueUp2", 0.570000f), new VisemeMapping("tongueUp3", 0.690000f), new VisemeMapping("noseDown", 0.210000f) } },
+ { "LeftShoulderTwist1", new[] { new VisemeMapping("O_mouth", 0.380000f), new VisemeMapping("jawOpen", 0.173363f), new VisemeMapping("lowerLipCurlOut", 1.000000f) } },
+ { "LeftThumbFinger", new[] { new VisemeMapping("smileRight", 1.000000f), new VisemeMapping("smileLeft", 1.000000f), new VisemeMapping("sneerRight", 0.300000f), new VisemeMapping("sneerLeft", 0.300000f), new VisemeMapping("jawOpen", 0.020000f) } },
+ { "LeftThumbFinger1", new[] { new VisemeMapping("O_mouth", 0.300000f), new VisemeMapping("jawOpen", 0.080357f) } },
+ { "LeftWrist", new[] { new VisemeMapping("upperLipCurlOut", 1.000000f), new VisemeMapping("lowerLipCurlOut", 1.000000f), new VisemeMapping("O_mouth", 0.220000f) } },
+ { "LipCorner_left", new[] { new VisemeMapping("jawOpen", 0.010000f) } },
+ { "LipCorner_right", new[] { new VisemeMapping("sneerRight", 0.150000f), new VisemeMapping("sneerLeft", 0.150000f), new VisemeMapping("pucker", 1.000000f), new VisemeMapping("jawOpen", 0.046875f), new VisemeMapping("upperLipCurlIn", 1.000000f), new VisemeMapping("lowerLipCurlIn", 1.000000f) } },
+ { "LipCorner1_left", new[] { new VisemeMapping("lowerLipCurlOut", 0.120000f), new VisemeMapping("lowerLipCurlIn", 0.130000f), new VisemeMapping("jawOpen", 0.020000f), new VisemeMapping("tongueUp", 0.660000f) } },
+ { "LipCorner1_right", new[] { new VisemeMapping("sneerRight", 0.150000f), new VisemeMapping("sneerLeft", 0.150000f), new VisemeMapping("pucker", 1.000000f), new VisemeMapping("jawOpen", 0.046875f), new VisemeMapping("upperLipCurlIn", 1.000000f), new VisemeMapping("lowerLipCurlIn", 1.000000f) } },
+ { "LowerBack", new[] { new VisemeMapping("upperLipCurlIn", 0.760000f), new VisemeMapping("lowerLipCurlIn", 0.760000f), new VisemeMapping("jawOpen", 0.030000f), new VisemeMapping("pucker", 0.170000f), new VisemeMapping("tongueUp1", 0.300000f), new VisemeMapping("tongueUp2", 0.570000f), new VisemeMapping("tongueUp3", 0.690000f), new VisemeMapping("noseDown", 0.210000f) } },
+ { "LowerCheek_left", new[] { new VisemeMapping("smileRight", 0.690000f), new VisemeMapping("smileLeft", 0.690000f), new VisemeMapping("sneerRight", 0.200000f), new VisemeMapping("sneerLeft", 0.200000f), new VisemeMapping("upperLipCurlIn", 0.420000f), new VisemeMapping("tongueUp", 0.720000f) } },
+ { "LowerCheek_right", new[] { new VisemeMapping("jawOpen", 0.010000f) } },
+ { "Neck", new[] { new VisemeMapping("upperLipCurlOut", 0.500000f), new VisemeMapping("lowerLipCurlOut", 0.520000f), new VisemeMapping("jawOpen", 0.010000f), new VisemeMapping("O_mouth", 0.160000f) } },
+ { "outBrow_left", new[] { new VisemeMapping("sneerRight", 0.100000f), new VisemeMapping("sneerLeft", 0.100000f), new VisemeMapping("upperLipCurlOut", 0.350000f), new VisemeMapping("lowerLipCurlOut", 0.390000f), new VisemeMapping("O_mouth", 0.080000f) } },
+ { "outBrow_right", new[] { new VisemeMapping("smileRight", 1.000000f), new VisemeMapping("smileLeft", 1.000000f), new VisemeMapping("sneerRight", 0.260000f), new VisemeMapping("sneerLeft", 0.260000f), new VisemeMapping("jawOpen", 0.091518f) } },
+ { "Pelvis", new[] { new VisemeMapping("smileRight", 1.000000f), new VisemeMapping("smileLeft", 1.000000f), new VisemeMapping("jawOpen", 0.098958f), new VisemeMapping("lowerLipCurlIn", 1.000000f) } },
+ { "RightBangle", new[] { new VisemeMapping("jawOpen", 0.020000f), new VisemeMapping("O_mouth", 0.260000f), new VisemeMapping("tongueUp1", 0.280000f), new VisemeMapping("tongueUp2", 0.330000f), new VisemeMapping("tongueUp3", 0.400000f), new VisemeMapping("tongueForward", 0.820000f) } },
+ { "RightCollar", new[] { new VisemeMapping("O_mouth", 0.380000f), new VisemeMapping("jawOpen", 0.173363f), new VisemeMapping("lowerLipCurlOut", 1.000000f) } },
+ { "RightDigit", new[] { new VisemeMapping("jawOpen", 0.173363f), new VisemeMapping("noseUp", 0.040000f) } },
+ { "RightElbow", new[] { new VisemeMapping("pucker", 1.000000f), new VisemeMapping("upperLipCurlIn", 1.000000f), new VisemeMapping("lowerLipCurlOut", 0.005952f), new VisemeMapping("lowerLipCurlIn", 0.716518f), new VisemeMapping("noseDown", 0.100000f) } },
+ { "RightFlap", new[] { new VisemeMapping("smileRight", 1.000000f), new VisemeMapping("smileLeft", 1.000000f), new VisemeMapping("pucker", 0.900000f), new VisemeMapping("O_mouth", 0.480000f), new VisemeMapping("jawOpen", 0.030000f), new VisemeMapping("upperLipCurlOut", 0.370000f), new VisemeMapping("lowerLipCurlOut", 0.390000f), new VisemeMapping("noseDown", 0.200000f) } },
+ { "RightIndexFinger", new[] { new VisemeMapping("smileRight", 0.710000f), new VisemeMapping("smileLeft", 0.710000f), new VisemeMapping("jawOpen", 0.121280f) } },
+ { "RightIndexFinger1", new[] { new VisemeMapping("smileRight", 0.710000f), new VisemeMapping("smileLeft", 0.710000f), new VisemeMapping("jawOpen", 0.121280f) } },
+ { "RightIndexFinger2", new[] { new VisemeMapping("jawOpen", 0.139881f) } },
+ { "RightPinkFinger", new[] { new VisemeMapping("smileRight", 1.000000f), new VisemeMapping("smileLeft", 1.000000f), new VisemeMapping("sneerRight", 0.250000f), new VisemeMapping("sneerLeft", 0.250000f), new VisemeMapping("jawOpen", 0.060000f), new VisemeMapping("jawForward", 0.380000f) } },
+ { "RightPinkFinger1", new[] { new VisemeMapping("pucker", 1.000000f), new VisemeMapping("upperLipCurlIn", 1.000000f), new VisemeMapping("lowerLipCurlOut", 0.005952f), new VisemeMapping("lowerLipCurlIn", 0.716518f), new VisemeMapping("noseDown", 0.100000f) } },
+ { "RightPinkFinger2", new[] { new VisemeMapping("pucker", 1.000000f), new VisemeMapping("O_mouth", 0.392857f), new VisemeMapping("jawOpen", 0.050595f), new VisemeMapping("upperLipCurlOut", 0.870000f), new VisemeMapping("lowerLipCurlOut", 0.850000f) } },
+ { "RightShoulder", new[] { new VisemeMapping("O_mouth", 1.000000f), new VisemeMapping("jawForward", 0.470000f), new VisemeMapping("upperLipCurlOut", 1.000000f), new VisemeMapping("lowerLipCurlOut", 1.000000f) } },
+ { "RightShoulderTwist1", new[] { new VisemeMapping("smileRight", 1.000000f), new VisemeMapping("smileLeft", 1.000000f), new VisemeMapping("jawOpen", 0.098958f), new VisemeMapping("lowerLipCurlIn", 1.000000f) } },
+ { "RightThumbFinger", new[] { new VisemeMapping("O_mouth", 0.380000f), new VisemeMapping("jawOpen", 0.173363f), new VisemeMapping("lowerLipCurlOut", 1.000000f) } },
+ { "RightThumbFinger1", new[] { new VisemeMapping("O_mouth", 0.380000f), new VisemeMapping("jawOpen", 0.173363f), new VisemeMapping("lowerLipCurlOut", 1.000000f) } },
+ { "RightWrist", new[] { new VisemeMapping("O_mouth", 0.380000f), new VisemeMapping("jawOpen", 0.173363f), new VisemeMapping("lowerLipCurlOut", 1.000000f) } },
+ { "Root", new[] { new VisemeMapping("jawForward", 0.010000f) } },
+ { "SFX_Elcor", new[] { new VisemeMapping("tongueUp", 0.010000f) } },
+ { "Sneer", new[] { new VisemeMapping("sneerRight", 0.200000f), new VisemeMapping("sneerLeft", 0.200000f), new VisemeMapping("tongueUp1", 0.190000f), new VisemeMapping("tongueUp2", 0.240000f), new VisemeMapping("tongueUp3", 0.380000f), new VisemeMapping("tongueForward", 0.850000f) } },
+ { "Throat", new[] { new VisemeMapping("pucker", 0.530506f), new VisemeMapping("jawOpen", 0.054315f), new VisemeMapping("upperLipCurlIn", 0.593750f), new VisemeMapping("lowerLipCurlIn", 1.000000f) } },
+ { "Throat1", new[] { new VisemeMapping("pucker", 1.000000f), new VisemeMapping("upperLipCurlIn", 1.000000f), new VisemeMapping("lowerLipCurlOut", 0.005952f), new VisemeMapping("lowerLipCurlIn", 0.716518f), new VisemeMapping("noseDown", 0.100000f) } },
+ };
+
+ ///
+ /// All viseme animation names used in lip sync for Elcor
+ ///
+ public static readonly string[] ElcorVisemes =
+ [
+ "sneerRight",
+ "sneerLeft",
+ "O_mouth",
+ "upperLipCurlOut",
+ "lowerLipCurlOut",
+ "smileRight",
+ "smileLeft",
+ "jawOpen",
+ "pucker",
+ "upperLipCurlIn",
+ "lowerLipCurlIn",
+ "noseDown",
+ "noseUp",
+ "tongueUp",
+ "tongueUp1",
+ "tongueUp2",
+ "tongueUp3",
+ "tongueForward",
+ "jawForward"
+ ];
+
+ ///
+ /// Hanar phoneme to viseme mappings - from SFX_Hannar_FaceFX data.
+ /// Note: Hanar use specialized phoneme identifiers in FaceFX mapping.
+ ///
+ public static readonly Dictionary HanarPhonemeMap = new()
+ {
+ // Hanar phoneme mappings from SFX_Hannar_FaceFX
+ { "cheekPuff", new[] { new VisemeMapping("jawForward", 0.010000f) } },
+ { "E_Angry_Interested", new[] { new VisemeMapping("smileRight", 1.000000f), new VisemeMapping("smileLeft", 1.000000f), new VisemeMapping("sneerRight", 0.300000f), new VisemeMapping("sneerLeft", 0.300000f), new VisemeMapping("jawOpen", 0.020000f) } },
+ { "E_Angry_Question", new[] { new VisemeMapping("O_mouth", 0.300000f), new VisemeMapping("jawOpen", 0.080357f) } },
+ { "E_Angry_Rage", new[] { new VisemeMapping("upperLipCurlIn", 0.760000f), new VisemeMapping("lowerLipCurlIn", 0.760000f), new VisemeMapping("jawOpen", 0.030000f), new VisemeMapping("pucker", 0.170000f), new VisemeMapping("tongueUp1", 0.300000f), new VisemeMapping("tongueUp2", 0.570000f), new VisemeMapping("tongueUp3", 0.690000f), new VisemeMapping("noseDown", 0.210000f) } },
+ { "E_Angry_Shocked", new[] { new VisemeMapping("upperLipCurlOut", 1.000000f), new VisemeMapping("lowerLipCurlOut", 1.000000f), new VisemeMapping("O_mouth", 0.220000f) } },
+ { "E_Angry_Squint", new[] { new VisemeMapping("upperLipCurlOut", 1.000000f), new VisemeMapping("lowerLipCurlOut", 1.000000f), new VisemeMapping("O_mouth", 0.300000f) } },
+ { "E_flirt", new[] { new VisemeMapping("sneerRight", 0.200000f), new VisemeMapping("sneerLeft", 0.200000f), new VisemeMapping("tongueUp1", 0.190000f), new VisemeMapping("tongueUp2", 0.240000f), new VisemeMapping("tongueUp3", 0.380000f), new VisemeMapping("tongueForward", 0.850000f) } },
+ { "E_GESTURE_HeadLeft", new[] { new VisemeMapping("jawOpen", 0.139881f) } },
+ { "E_GESTURE_HeadRight", new[] { new VisemeMapping("smileRight", 1.000000f), new VisemeMapping("smileLeft", 1.000000f), new VisemeMapping("jawOpen", 0.098958f), new VisemeMapping("lowerLipCurlIn", 1.000000f) } },
+ { "E_GESTURE_NeckBackLeft", new[] { new VisemeMapping("smileRight", 1.000000f), new VisemeMapping("smileLeft", 1.000000f), new VisemeMapping("sneerRight", 0.250000f), new VisemeMapping("sneerLeft", 0.250000f), new VisemeMapping("jawOpen", 0.060000f), new VisemeMapping("jawForward", 0.380000f) } },
+ { "E_GESTURE_NeckBackRight", new[] { new VisemeMapping("jawOpen", 0.173363f), new VisemeMapping("noseUp", 0.040000f) } },
+ { "E_GESTURE_NeckForwardLeft", new[] { new VisemeMapping("O_mouth", 0.380000f), new VisemeMapping("jawOpen", 0.173363f), new VisemeMapping("lowerLipCurlOut", 1.000000f) } },
+ { "E_GESTURE_NeckForwardRight", new[] { new VisemeMapping("O_mouth", 0.380000f), new VisemeMapping("jawOpen", 0.173363f), new VisemeMapping("lowerLipCurlOut", 1.000000f) } },
+ { "E_Happy_Diabolical", new[] { new VisemeMapping("O_mouth", 0.380000f), new VisemeMapping("jawOpen", 0.173363f), new VisemeMapping("lowerLipCurlOut", 1.000000f) } },
+ { "E_Happy_Dissapointed", new[] { new VisemeMapping("O_mouth", 0.380000f), new VisemeMapping("jawOpen", 0.173363f), new VisemeMapping("lowerLipCurlOut", 1.000000f) } },
+ { "E_Happy_Fake", new[] { new VisemeMapping("O_mouth", 0.300000f), new VisemeMapping("jawOpen", 0.080357f) } },
+ { "E_Happy_Interested", new[] { new VisemeMapping("O_mouth", 0.300000f), new VisemeMapping("jawOpen", 0.080357f) } },
+ { "E_Happy_OverJoyed", new[] { new VisemeMapping("O_mouth", 0.300000f), new VisemeMapping("jawOpen", 0.080357f) } },
+ { "E_Happy_Question", new[] { new VisemeMapping("O_mouth", 0.380000f), new VisemeMapping("jawOpen", 0.173363f), new VisemeMapping("lowerLipCurlOut", 1.000000f) } },
+ { "E_Neck_Pitch", new[] { new VisemeMapping("O_mouth", 1.000000f), new VisemeMapping("jawOpen", 0.098958f), new VisemeMapping("upperLipCurlOut", 1.000000f), new VisemeMapping("lowerLipCurlOut", 1.000000f) } },
+ { "E_Neck_Yaw", new[] { new VisemeMapping("pucker", 1.000000f), new VisemeMapping("upperLipCurlIn", 1.000000f), new VisemeMapping("lowerLipCurlOut", 0.005952f), new VisemeMapping("lowerLipCurlIn", 0.716518f), new VisemeMapping("noseDown", 0.100000f) } },
+ { "Emissive", new[] { new VisemeMapping("upperLipCurlOut", 0.370000f), new VisemeMapping("lowerLipCurlOut", 0.390000f), new VisemeMapping("pucker", 0.900000f), new VisemeMapping("O_mouth", 0.330000f), new VisemeMapping("noseDown", 0.200000f) } },
+ { "Emote_Blender", new[] { new VisemeMapping("jawOpen", 0.020000f), new VisemeMapping("O_mouth", 0.260000f), new VisemeMapping("tongueUp1", 0.280000f), new VisemeMapping("tongueUp2", 0.330000f), new VisemeMapping("tongueUp3", 0.400000f), new VisemeMapping("tongueForward", 0.820000f) } },
+ { "EmotionBlender", new[] { new VisemeMapping("upperLipCurlOut", 0.500000f), new VisemeMapping("lowerLipCurlOut", 0.520000f), new VisemeMapping("jawOpen", 0.010000f), new VisemeMapping("O_mouth", 0.160000f) } },
+ { "Emphasis_Head_Pitch", new[] { new VisemeMapping("smileRight", 1.000000f), new VisemeMapping("smileLeft", 1.000000f), new VisemeMapping("sneerRight", 0.150000f), new VisemeMapping("sneerLeft", 0.150000f), new VisemeMapping("jawOpen", 0.120000f) } },
+ { "Emphasis_Head_Yaw", new[] { new VisemeMapping("O_mouth", 0.380000f), new VisemeMapping("jawOpen", 0.173363f), new VisemeMapping("lowerLipCurlOut", 1.000000f) } },
+ { "FXA_Anim", new[] { new VisemeMapping("smileRight", 1.000000f), new VisemeMapping("smileLeft", 1.000000f), new VisemeMapping("sneerRight", 0.260000f), new VisemeMapping("sneerLeft", 0.260000f), new VisemeMapping("jawOpen", 0.091518f) } },
+ { "FXA_Group", new[] { new VisemeMapping("smileRight", 1.000000f), new VisemeMapping("smileLeft", 1.000000f), new VisemeMapping("sneerRight", 0.210000f), new VisemeMapping("sneerLeft", 0.210000f), new VisemeMapping("jawOpen", 0.084077f) } },
+ { "FXA_Path", new[] { new VisemeMapping("tongueUp1", 0.180000f), new VisemeMapping("tongueUp2", 0.270000f), new VisemeMapping("tongueUp3", 0.360000f), new VisemeMapping("tongueForward", 0.890000f) } },
+ { "G_WeightShiftLeft", new[] { new VisemeMapping("pucker", 0.530506f), new VisemeMapping("jawOpen", 0.054315f), new VisemeMapping("upperLipCurlIn", 0.593750f), new VisemeMapping("lowerLipCurlIn", 1.000000f) } },
+ { "happy_alarmed", new[] { new VisemeMapping("smileRight", 1.000000f), new VisemeMapping("smileLeft", 1.000000f), new VisemeMapping("pucker", 0.900000f), new VisemeMapping("O_mouth", 0.480000f), new VisemeMapping("jawOpen", 0.030000f), new VisemeMapping("upperLipCurlOut", 0.370000f), new VisemeMapping("lowerLipCurlOut", 0.390000f), new VisemeMapping("noseDown", 0.200000f) } },
+ { "head_RX-", new[] { new VisemeMapping("smileRight", 1.000000f), new VisemeMapping("smileLeft", 1.000000f), new VisemeMapping("sneerRight", 0.150000f), new VisemeMapping("sneerLeft", 0.150000f), new VisemeMapping("jawOpen", 0.121280f) } },
+ { "head_RX+", new[] { new VisemeMapping("smileRight", 1.000000f), new VisemeMapping("smileLeft", 1.000000f), new VisemeMapping("jawOpen", 0.098958f), new VisemeMapping("lowerLipCurlIn", 1.000000f) } },
+ { "Head_Yaw", new[] { new VisemeMapping("smileRight", 1.000000f), new VisemeMapping("smileLeft", 1.000000f), new VisemeMapping("jawOpen", 0.098958f), new VisemeMapping("lowerLipCurlIn", 1.000000f) } },
+ { "InnerBrowLeft_Out", new[] { new VisemeMapping("smileRight", 0.430000f), new VisemeMapping("smileLeft", 0.430000f), new VisemeMapping("pucker", 1.000000f), new VisemeMapping("upperLipCurlIn", 1.000000f), new VisemeMapping("lowerLipCurlIn", 1.000000f) } },
+ { "InnerBrowRight_Out", new[] { new VisemeMapping("upperLipCurlOut", 0.500000f), new VisemeMapping("lowerLipCurlOut", 0.520000f), new VisemeMapping("jawOpen", 0.010000f), new VisemeMapping("O_mouth", 0.160000f) } },
+ { "jawForward", new[] { new VisemeMapping("smileRight", 1.000000f), new VisemeMapping("smileLeft", 1.000000f), new VisemeMapping("jawOpen", 0.125000f) } },
+ { "jawRotate", new[] { new VisemeMapping("O_mouth", 1.000000f), new VisemeMapping("jawForward", 0.470000f), new VisemeMapping("upperLipCurlOut", 1.000000f), new VisemeMapping("lowerLipCurlOut", 1.000000f) } },
+ { "jawRotateDown", new[] { new VisemeMapping("jawOpen", 0.010000f) } },
+ { "jawRotateUp", new[] { new VisemeMapping("jawOpen", 0.010000f) } },
+ { "jawSideLeft", new[] { new VisemeMapping("sneerRight", 0.250000f), new VisemeMapping("sneerLeft", 0.250000f), new VisemeMapping("tongueUp1", 0.510000f), new VisemeMapping("tongueUp2", 0.530000f), new VisemeMapping("tongueUp3", 0.700000f) } },
+ { "jawSideRight", new[] { new VisemeMapping("smileRight", 0.690000f), new VisemeMapping("smileLeft", 0.690000f), new VisemeMapping("sneerRight", 0.200000f), new VisemeMapping("sneerLeft", 0.200000f), new VisemeMapping("upperLipCurlIn", 0.420000f), new VisemeMapping("tongueUp", 0.720000f) } },
+ { "LipSynch", new[] { new VisemeMapping("pucker", 0.534226f), new VisemeMapping("O_mouth", 1.000000f), new VisemeMapping("jawOpen", 0.039435f), new VisemeMapping("upperLipCurlOut", 1.000000f), new VisemeMapping("lowerLipCurlOut", 1.000000f) } },
+ { "lowerLipCurlIn", new[] { new VisemeMapping("sneerRight", 0.150000f), new VisemeMapping("sneerLeft", 0.150000f), new VisemeMapping("pucker", 1.000000f), new VisemeMapping("jawOpen", 0.046875f), new VisemeMapping("upperLipCurlIn", 1.000000f), new VisemeMapping("lowerLipCurlIn", 1.000000f) } },
+ { "lowerLipDownLeft", new[] { new VisemeMapping("upperLipCurlIn", 0.760000f), new VisemeMapping("lowerLipCurlIn", 0.760000f), new VisemeMapping("jawOpen", 0.030000f), new VisemeMapping("pucker", 0.170000f), new VisemeMapping("tongueUp1", 0.300000f), new VisemeMapping("tongueUp2", 0.570000f), new VisemeMapping("tongueUp3", 0.690000f), new VisemeMapping("noseDown", 0.210000f) } },
+ { "lowerLipDownRight", new[] { new VisemeMapping("sneerRight", 0.130000f), new VisemeMapping("sneerLeft", 0.130000f), new VisemeMapping("upperLipCurlOut", 0.370000f), new VisemeMapping("lowerLipCurlOut", 0.230000f), new VisemeMapping("jawOpen", 0.050000f), new VisemeMapping("tongueUp1", 0.390000f), new VisemeMapping("tongueUp2", 0.660000f), new VisemeMapping("tongueUp3", 0.760000f) } },
+ { "Material_Slot_Id", new[] { new VisemeMapping("sneerRight", 0.250000f), new VisemeMapping("sneerLeft", 0.250000f), new VisemeMapping("O_mouth", 1.000000f), new VisemeMapping("upperLipCurlOut", 1.000000f), new VisemeMapping("lowerLipCurlOut", 1.000000f) } },
+ { "neck_RX-", new[] { new VisemeMapping("smileRight", 0.710000f), new VisemeMapping("smileLeft", 0.710000f), new VisemeMapping("jawOpen", 0.121280f) } },
+ { "neck_RX+", new[] { new VisemeMapping("pucker", 1.000000f), new VisemeMapping("O_mouth", 0.392857f), new VisemeMapping("jawOpen", 0.050595f), new VisemeMapping("upperLipCurlOut", 0.870000f), new VisemeMapping("lowerLipCurlOut", 0.850000f) } },
+ { "neck_RZ+", new[] { new VisemeMapping("O_mouth", 1.000000f), new VisemeMapping("jawOpen", 0.136161f), new VisemeMapping("upperLipCurlOut", 1.000000f), new VisemeMapping("lowerLipCurlOut", 1.000000f) } },
+ { "neckYaw_sum", new[] { new VisemeMapping("smileRight", 0.710000f), new VisemeMapping("smileLeft", 0.710000f), new VisemeMapping("jawOpen", 0.121280f) } },
+ { "Orientation_Head_Pitch", new[] { new VisemeMapping("pucker", 1.000000f), new VisemeMapping("O_mouth", 1.000000f), new VisemeMapping("jawOpen", 0.080357f), new VisemeMapping("upperLipCurlOut", 0.450000f), new VisemeMapping("lowerLipCurlOut", 0.510000f) } },
+ { "Orientation_Head_Yaw", new[] { new VisemeMapping("pucker", 1.000000f), new VisemeMapping("upperLipCurlIn", 1.000000f), new VisemeMapping("lowerLipCurlOut", 0.005952f), new VisemeMapping("lowerLipCurlIn", 0.716518f), new VisemeMapping("noseDown", 0.100000f) } },
+ { "Parameter_Name", new[] { new VisemeMapping("sneerRight", 0.100000f), new VisemeMapping("sneerLeft", 0.100000f), new VisemeMapping("upperLipCurlOut", 0.350000f), new VisemeMapping("lowerLipCurlOut", 0.390000f), new VisemeMapping("O_mouth", 0.080000f) } },
+ { "Red", new[] { new VisemeMapping("sneerRight", 0.100000f), new VisemeMapping("sneerLeft", 0.100000f), new VisemeMapping("upperLipCurlOut", 0.350000f), new VisemeMapping("lowerLipCurlOut", 0.390000f), new VisemeMapping("O_mouth", 0.080000f) } },
+ { "S_Angry", new[] { new VisemeMapping("O_mouth", 0.300000f), new VisemeMapping("jawOpen", 0.080357f) } },
+ { "S_Happy", new[] { new VisemeMapping("O_mouth", 0.380000f), new VisemeMapping("jawOpen", 0.173363f), new VisemeMapping("lowerLipCurlOut", 1.000000f) } },
+ { "sad_angry", new[] { new VisemeMapping("jawOpen", 0.010000f) } },
+ { "SFX_Hannar", new[] { new VisemeMapping("tongueUp1", 0.010000f) } },
+ { "sneerLeft", new[] { new VisemeMapping("sneerRight", 0.100000f), new VisemeMapping("sneerLeft", 0.100000f), new VisemeMapping("upperLipCurlOut", 0.350000f), new VisemeMapping("lowerLipCurlOut", 0.390000f), new VisemeMapping("O_mouth", 0.080000f) } },
+ { "Stage_Blender", new[] { new VisemeMapping("pucker", 1.000000f), new VisemeMapping("upperLipCurlIn", 1.000000f), new VisemeMapping("lowerLipCurlOut", 0.005952f), new VisemeMapping("lowerLipCurlIn", 0.716518f), new VisemeMapping("noseDown", 0.100000f) } },
+ { "talk_shade", new[] { new VisemeMapping("upperLipCurlOut", 0.370000f), new VisemeMapping("lowerLipCurlOut", 0.390000f), new VisemeMapping("pucker", 0.900000f), new VisemeMapping("O_mouth", 0.330000f), new VisemeMapping("noseDown", 0.200000f) } },
+ { "tongueUp", new[] { new VisemeMapping("upperLipCurlIn", 0.760000f), new VisemeMapping("lowerLipCurlIn", 0.760000f), new VisemeMapping("pucker", 0.170000f), new VisemeMapping("noseDown", 0.200000f) } },
+ { "upperLipCurlIn", new[] { new VisemeMapping("sneerRight", 0.150000f), new VisemeMapping("sneerLeft", 0.150000f), new VisemeMapping("pucker", 1.000000f), new VisemeMapping("jawOpen", 0.046875f), new VisemeMapping("upperLipCurlIn", 1.000000f), new VisemeMapping("lowerLipCurlIn", 1.000000f) } },
+ { "upperLipCurlOut", new[] { new VisemeMapping("lowerLipCurlOut", 0.120000f), new VisemeMapping("lowerLipCurlIn", 0.130000f), new VisemeMapping("jawOpen", 0.020000f), new VisemeMapping("tongueUp", 0.660000f) } },
+ };
+
+ ///
+ /// All viseme animation names used in lip sync for Hanar
+ ///
+ public static readonly string[] HanarVisemes =
+ [
+ "smileRight",
+ "smileLeft",
+ "sneerRight",
+ "sneerLeft",
+ "jawOpen",
+ "jawForward",
+ "O_mouth",
+ "upperLipCurlIn",
+ "lowerLipCurlIn",
+ "upperLipCurlOut",
+ "lowerLipCurlOut",
+ "pucker",
+ "tongueUp",
+ "tongueUp1",
+ "tongueUp2",
+ "tongueUp3",
+ "tongueForward",
+ "noseDown",
+ "noseUp"
+ ];
+
+ ///
+ /// Volus phoneme to viseme mappings - from SFX_Volus_FaceFX data.
+ /// Note: Volus use bone names as phoneme identifiers in FaceFX mapping.
+ ///
+ public static readonly Dictionary VolusPhonemeMap = new()
+ {
+ // Volus phoneme mappings from SFX_Volus_FaceFX
+ { "cheekPuff", new[] { new VisemeMapping("smileRight", 0.050000f), new VisemeMapping("smileLeft", 0.050000f), new VisemeMapping("frownRight", 0.150000f), new VisemeMapping("frownLeft", 0.150000f), new VisemeMapping("jawOpen", 0.070000f), new VisemeMapping("jawRotate", 0.160000f), new VisemeMapping("mouthDownLeft", 0.220000f), new VisemeMapping("mouthDownRight", 0.210000f) } },
+ { "Chest", new[] { new VisemeMapping("smileRight", 0.040000f), new VisemeMapping("smileLeft", 0.040000f), new VisemeMapping("sneerRight", 0.100000f), new VisemeMapping("sneerLeft", 0.120000f), new VisemeMapping("frownRight", 0.250000f), new VisemeMapping("frownLeft", 0.250000f), new VisemeMapping("jawForward", 0.350000f) } },
+ { "Chest1", new[] { new VisemeMapping("sneerRight", 0.110000f), new VisemeMapping("sneerLeft", 0.100000f), new VisemeMapping("frownRight", 0.340000f), new VisemeMapping("frownLeft", 0.290000f), new VisemeMapping("jawOpen", 0.040000f), new VisemeMapping("jawRotate", 0.060000f), new VisemeMapping("tongueUp", 1.000000f), new VisemeMapping("mouthDownLeft", 0.210000f), new VisemeMapping("mouthDownRight", 0.200000f) } },
+ { "Chest2", new[] { new VisemeMapping("frownRight", 0.370000f), new VisemeMapping("frownLeft", 0.250000f), new VisemeMapping("jawOpen", 0.050000f) } },
+ { "Head", new[] { new VisemeMapping("jawOpen", 0.070000f), new VisemeMapping("O_mouth", 0.230000f), new VisemeMapping("jawRotate", 0.030000f), new VisemeMapping("tongueUp", 1.000000f) } },
+ { "InnerBrowRight_Out", new[] { new VisemeMapping("jawOpen", 0.060000f), new VisemeMapping("pucker", 0.270000f), new VisemeMapping("O_mouth", 0.310000f), new VisemeMapping("jawRotate", 0.190000f) } },
+ { "LeftAnkle", new[] { new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.150000f), new VisemeMapping("jawOpen", 0.060000f), new VisemeMapping("jawRotate", 0.100000f) } },
+ { "LeftCollar", new[] { new VisemeMapping("jawOpen", 0.040000f), new VisemeMapping("O_mouth", 0.360000f), new VisemeMapping("jawRotate", 0.110000f) } },
+ { "LeftElbow", new[] { new VisemeMapping("sneerRight", 0.070000f), new VisemeMapping("sneerLeft", 0.060000f), new VisemeMapping("upperLipCurlOut", 0.520000f), new VisemeMapping("lowerLipCurlIn", 1.000000f) } },
+ { "LeftElbowTwist1", new[] { new VisemeMapping("frownRight", 0.370000f), new VisemeMapping("frownLeft", 0.370000f), new VisemeMapping("upperLipCurlIn", 0.330000f), new VisemeMapping("lowerLipCurlIn", 0.130000f), new VisemeMapping("pucker", 0.130000f), new VisemeMapping("jawRotate", 0.220000f) } },
+ { "LeftElbowTwist2", new[] { new VisemeMapping("frownRight", 0.370000f), new VisemeMapping("frownLeft", 0.370000f), new VisemeMapping("pucker", 0.170000f), new VisemeMapping("jawRotate", 0.190000f) } },
+ { "LeftFlap", new[] { new VisemeMapping("frownRight", 0.250000f), new VisemeMapping("frownLeft", 0.350000f), new VisemeMapping("lowerLipCurlIn", 0.260000f), new VisemeMapping("jawOpen", 0.060000f) } },
+ { "LeftHip", new[] { new VisemeMapping("pucker", 0.760000f) } },
+ { "LeftHipTwist1", new[] { new VisemeMapping("lowerLipCurlOut", 0.270000f), new VisemeMapping("jawOpen", 0.080000f), new VisemeMapping("jawRotate", 0.060000f), new VisemeMapping("mouthDownLeft", 0.180000f), new VisemeMapping("mouthDownRight", 0.190000f) } },
+ { "LeftIndexFinger", new[] { new VisemeMapping("sneerRight", 0.070000f), new VisemeMapping("sneerLeft", 0.070000f), new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.100000f), new VisemeMapping("lowerLipCurlOut", 0.300000f), new VisemeMapping("jawForward", 0.340000f), new VisemeMapping("pucker", 0.250000f), new VisemeMapping("O_mouth", 0.420000f) } },
+ { "LeftIndexFinger1", new[] { new VisemeMapping("frownRight", 0.370000f), new VisemeMapping("frownLeft", 0.250000f), new VisemeMapping("jawOpen", 0.050000f) } },
+ { "LeftIndexFinger2", new[] { new VisemeMapping("frownRight", 0.370000f), new VisemeMapping("frownLeft", 0.250000f), new VisemeMapping("jawOpen", 0.050000f) } },
+ { "LeftKnee", new[] { new VisemeMapping("upperLipCurlOut", 0.220000f), new VisemeMapping("lowerLipCurlOut", 0.510000f), new VisemeMapping("jawOpen", 0.040000f), new VisemeMapping("O_mouth", 0.370000f), new VisemeMapping("jawRotate", 0.150000f) } },
+ { "LeftPinkFinger", new[] { new VisemeMapping("smileRight", 0.050000f), new VisemeMapping("smileLeft", 0.050000f), new VisemeMapping("sneerRight", 0.180000f), new VisemeMapping("sneerLeft", 0.210000f), new VisemeMapping("frownRight", 0.370000f), new VisemeMapping("frownLeft", 0.350000f), new VisemeMapping("jawForward", 0.020000f) } },
+ { "LeftPinkFinger1", new[] { new VisemeMapping("smileRight", 0.060000f), new VisemeMapping("smileLeft", 0.060000f), new VisemeMapping("sneerRight", 0.130000f), new VisemeMapping("sneerLeft", 0.150000f), new VisemeMapping("frownRight", 0.250000f), new VisemeMapping("frownLeft", 0.250000f), new VisemeMapping("jawForward", 0.220000f) } },
+ { "LeftPinkFinger2", new[] { new VisemeMapping("sneerRight", 0.120000f), new VisemeMapping("sneerLeft", 0.100000f), new VisemeMapping("frownRight", 0.120000f), new VisemeMapping("frownLeft", 0.100000f), new VisemeMapping("O_mouth", 0.540000f) } },
+ { "LeftShoulder", new[] { new VisemeMapping("jawOpen", 0.005000f) } },
+ { "LeftShoulderTwist1", new[] { new VisemeMapping("jawOpen", 0.040000f), new VisemeMapping("O_mouth", 0.360000f), new VisemeMapping("jawRotate", 0.110000f) } },
+ { "LeftShoulderTwist2", new[] { new VisemeMapping("frownRight", 0.370000f), new VisemeMapping("frownLeft", 0.370000f), new VisemeMapping("jawRotate", 0.100000f) } },
+ { "LeftThumbFinger", new[] { new VisemeMapping("sneerRight", 0.060000f), new VisemeMapping("sneerLeft", 0.040000f), new VisemeMapping("upperLipCurlOut", 0.490000f), new VisemeMapping("lowerLipCurlIn", 1.000000f), new VisemeMapping("jawClench", 0.170000f) } },
+ { "LeftThumbFinger1", new[] { new VisemeMapping("jawOpen", 0.070000f), new VisemeMapping("O_mouth", 0.310000f), new VisemeMapping("jawRotate", 0.070000f), new VisemeMapping("tongueUp", 1.000000f) } },
+ { "LeftThumbFinger2", new[] { new VisemeMapping("upperLipCurlOut", 0.430000f), new VisemeMapping("jawOpen", 0.060000f), new VisemeMapping("O_mouth", 0.160000f), new VisemeMapping("jawRotate", 0.070000f), new VisemeMapping("tongueUp", 1.000000f) } },
+ { "LeftToe", new[] { new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.150000f), new VisemeMapping("jawOpen", 0.060000f), new VisemeMapping("jawRotate", 0.100000f) } },
+ { "LeftWrist", new[] { new VisemeMapping("sneerRight", 0.070000f), new VisemeMapping("sneerLeft", 0.060000f), new VisemeMapping("upperLipCurlOut", 0.520000f), new VisemeMapping("lowerLipCurlIn", 1.000000f) } },
+ { "LowerBack", new[] { new VisemeMapping("upperLipCurlIn", 0.110000f), new VisemeMapping("lowerLipCurlIn", 0.110000f), new VisemeMapping("jawClench", 0.190000f) } },
+ { "lowerLipDownLeft", new[] { new VisemeMapping("jawOpen", 0.040000f), new VisemeMapping("O_mouth", 0.520000f), new VisemeMapping("jawRotate", 0.240000f), new VisemeMapping("mouthDownLeft", 0.140000f), new VisemeMapping("mouthDownRight", 0.120000f) } },
+ { "lowerLipDownRight", new[] { new VisemeMapping("frownRight", 0.250000f), new VisemeMapping("frownLeft", 0.250000f), new VisemeMapping("jawOpen", 0.010000f), new VisemeMapping("jawRotate", 0.160000f), new VisemeMapping("mouthDownLeft", 0.230000f), new VisemeMapping("mouthDownRight", 0.220000f) } },
+ { "Neck", new[] { new VisemeMapping("frownRight", 0.370000f), new VisemeMapping("frownLeft", 0.370000f), new VisemeMapping("upperLipCurlIn", 0.330000f), new VisemeMapping("lowerLipCurlIn", 0.130000f), new VisemeMapping("pucker", 0.130000f), new VisemeMapping("jawRotate", 0.220000f) } },
+ { "Neck1", new[] { new VisemeMapping("upperLipCurlIn", 0.690000f), new VisemeMapping("lowerLipCurlIn", 0.890000f) } },
+ { "Pack", new[] { new VisemeMapping("lowerLipCurlOut", 0.570000f), new VisemeMapping("jawOpen", 0.050000f), new VisemeMapping("pucker", 0.160000f), new VisemeMapping("O_mouth", 0.480000f), new VisemeMapping("jawRotate", 0.140000f) } },
+ { "Pelvis", new[] { new VisemeMapping("pucker", 0.760000f) } },
+ { "Pouch", new[] { new VisemeMapping("smileLeft", 0.040000f), new VisemeMapping("frownRight", 0.250000f), new VisemeMapping("frownLeft", 0.250000f), new VisemeMapping("upperLipCurlIn", 0.190000f), new VisemeMapping("jawOpen", 0.040000f), new VisemeMapping("jawRotate", 0.160000f), new VisemeMapping("jawClench", 0.280000f) } },
+ { "RightAnkle", new[] { new VisemeMapping("frownRight", 0.110000f), new VisemeMapping("frownLeft", 0.110000f), new VisemeMapping("lowerLipCurlOut", 0.180000f), new VisemeMapping("jawOpen", 0.080000f), new VisemeMapping("pucker", 0.100000f), new VisemeMapping("jawRotate", 0.140000f), new VisemeMapping("mouthDownLeft", 0.170000f), new VisemeMapping("mouthDownRight", 0.140000f) } },
+ { "RightCollar", new[] { new VisemeMapping("frownRight", 0.250000f), new VisemeMapping("frownLeft", 0.150000f), new VisemeMapping("jawOpen", 0.040000f), new VisemeMapping("jawRotate", 0.060000f), new VisemeMapping("tongueUp", 1.000000f) } },
+ { "RightElbow", new[] { new VisemeMapping("frownRight", 0.370000f), new VisemeMapping("frownLeft", 0.370000f), new VisemeMapping("pucker", 0.170000f), new VisemeMapping("jawRotate", 0.190000f) } },
+ { "RightElbowTwist1", new[] { new VisemeMapping("frownRight", 0.110000f), new VisemeMapping("frownLeft", 0.110000f), new VisemeMapping("lowerLipCurlOut", 0.180000f), new VisemeMapping("jawOpen", 0.080000f), new VisemeMapping("pucker", 0.100000f), new VisemeMapping("jawRotate", 0.140000f), new VisemeMapping("mouthDownLeft", 0.170000f), new VisemeMapping("mouthDownRight", 0.140000f) } },
+ { "RightElbowTwist2", new[] { new VisemeMapping("frownRight", 0.110000f), new VisemeMapping("frownLeft", 0.110000f), new VisemeMapping("lowerLipCurlOut", 0.180000f), new VisemeMapping("jawOpen", 0.080000f), new VisemeMapping("pucker", 0.100000f), new VisemeMapping("jawRotate", 0.140000f), new VisemeMapping("mouthDownLeft", 0.170000f), new VisemeMapping("mouthDownRight", 0.140000f) } },
+ { "RightFlap", new[] { new VisemeMapping("jawOpen", 0.040000f), new VisemeMapping("O_mouth", 0.360000f), new VisemeMapping("jawRotate", 0.110000f) } },
+ { "RightHip", new[] { new VisemeMapping("frownRight", 0.110000f), new VisemeMapping("frownLeft", 0.110000f), new VisemeMapping("lowerLipCurlOut", 0.180000f), new VisemeMapping("jawOpen", 0.080000f), new VisemeMapping("pucker", 0.100000f), new VisemeMapping("jawRotate", 0.140000f), new VisemeMapping("mouthDownLeft", 0.170000f), new VisemeMapping("mouthDownRight", 0.140000f) } },
+ { "RightHipTwist1", new[] { new VisemeMapping("smileRight", 0.040000f), new VisemeMapping("smileLeft", 0.040000f), new VisemeMapping("frownRight", 0.370000f), new VisemeMapping("frownLeft", 0.370000f), new VisemeMapping("jawOpen", 0.030000f), new VisemeMapping("jawRotate", 0.200000f), new VisemeMapping("mouthDownLeft", 0.230000f), new VisemeMapping("mouthDownRight", 0.210000f) } },
+ { "RightIndexFinger", new[] { new VisemeMapping("frownRight", 0.090000f), new VisemeMapping("frownLeft", 0.110000f), new VisemeMapping("jawOpen", 0.070000f), new VisemeMapping("jawRotate", 0.060000f), new VisemeMapping("mouthDownLeft", 0.170000f), new VisemeMapping("mouthDownRight", 0.140000f) } },
+ { "RightIndexFinger1", new[] { new VisemeMapping("frownRight", 0.090000f), new VisemeMapping("frownLeft", 0.110000f), new VisemeMapping("jawOpen", 0.070000f), new VisemeMapping("jawRotate", 0.060000f), new VisemeMapping("mouthDownLeft", 0.170000f), new VisemeMapping("mouthDownRight", 0.140000f) } },
+ { "RightIndexFinger2", new[] { new VisemeMapping("frownRight", 0.090000f), new VisemeMapping("frownLeft", 0.110000f), new VisemeMapping("jawOpen", 0.070000f), new VisemeMapping("jawRotate", 0.060000f), new VisemeMapping("mouthDownLeft", 0.170000f), new VisemeMapping("mouthDownRight", 0.140000f) } },
+ { "RightKnee", new[] { new VisemeMapping("frownRight", 0.110000f), new VisemeMapping("frownLeft", 0.110000f), new VisemeMapping("lowerLipCurlOut", 0.180000f), new VisemeMapping("jawOpen", 0.080000f), new VisemeMapping("pucker", 0.100000f), new VisemeMapping("jawRotate", 0.140000f), new VisemeMapping("mouthDownLeft", 0.170000f), new VisemeMapping("mouthDownRight", 0.140000f) } },
+ { "RightPinkFinger", new[] { new VisemeMapping("frownRight", 0.090000f), new VisemeMapping("frownLeft", 0.110000f), new VisemeMapping("jawOpen", 0.070000f), new VisemeMapping("jawRotate", 0.060000f), new VisemeMapping("mouthDownLeft", 0.170000f), new VisemeMapping("mouthDownRight", 0.140000f) } },
+ { "RightPinkFinger1", new[] { new VisemeMapping("frownRight", 0.090000f), new VisemeMapping("frownLeft", 0.110000f), new VisemeMapping("jawOpen", 0.070000f), new VisemeMapping("jawRotate", 0.060000f), new VisemeMapping("mouthDownLeft", 0.170000f), new VisemeMapping("mouthDownRight", 0.140000f) } },
+ { "RightPinkFinger2", new[] { new VisemeMapping("smileRight", 0.070000f), new VisemeMapping("smileLeft", 0.070000f), new VisemeMapping("sneerRight", 0.120000f), new VisemeMapping("sneerLeft", 0.110000f), new VisemeMapping("frownRight", 0.250000f), new VisemeMapping("frownLeft", 0.370000f), new VisemeMapping("jawRotate", 0.250000f) } },
+ { "RightShoulder", new[] { new VisemeMapping("pucker", 0.760000f) } },
+ { "RightShoulderTwist1", new[] { new VisemeMapping("frownRight", 0.110000f), new VisemeMapping("frownLeft", 0.110000f), new VisemeMapping("lowerLipCurlOut", 0.180000f), new VisemeMapping("jawOpen", 0.080000f), new VisemeMapping("pucker", 0.100000f), new VisemeMapping("jawRotate", 0.140000f), new VisemeMapping("mouthDownLeft", 0.170000f), new VisemeMapping("mouthDownRight", 0.140000f) } },
+ { "RightShoulderTwist2", new[] { new VisemeMapping("frownRight", 0.110000f), new VisemeMapping("frownLeft", 0.110000f), new VisemeMapping("lowerLipCurlOut", 0.180000f), new VisemeMapping("jawOpen", 0.080000f), new VisemeMapping("pucker", 0.100000f), new VisemeMapping("jawRotate", 0.140000f), new VisemeMapping("mouthDownLeft", 0.170000f), new VisemeMapping("mouthDownRight", 0.140000f) } },
+ { "RightThumbFinger", new[] { new VisemeMapping("sneerRight", 0.090000f), new VisemeMapping("sneerLeft", 0.090000f), new VisemeMapping("frownRight", 0.150000f), new VisemeMapping("frownLeft", 0.070000f), new VisemeMapping("pucker", 0.120000f), new VisemeMapping("O_mouth", 0.270000f) } },
+ { "RightThumbFinger1", new[] { new VisemeMapping("sneerRight", 0.130000f), new VisemeMapping("sneerLeft", 0.100000f), new VisemeMapping("frownRight", 0.130000f), new VisemeMapping("frownLeft", 0.150000f), new VisemeMapping("lowerLipCurlOut", 0.160000f), new VisemeMapping("jawForward", 0.340000f), new VisemeMapping("O_mouth", 0.300000f), new VisemeMapping("mouthDownLeft", 0.070000f), new VisemeMapping("mouthDownRight", 0.060000f) } },
+ { "RightThumbFinger2", new[] { new VisemeMapping("smileRight", 0.020000f), new VisemeMapping("smileLeft", 0.020000f), new VisemeMapping("sneerRight", 0.080000f), new VisemeMapping("sneerLeft", 0.090000f), new VisemeMapping("frownRight", 0.370000f), new VisemeMapping("frownLeft", 0.370000f), new VisemeMapping("jawOpen", 0.020000f), new VisemeMapping("jawRotate", 0.160000f), new VisemeMapping("mouthDownLeft", 0.210000f), new VisemeMapping("mouthDownRight", 0.220000f) } },
+ { "RightToe", new[] { new VisemeMapping("jawOpen", 0.090000f), new VisemeMapping("O_mouth", 0.170000f), new VisemeMapping("jawRotate", 0.190000f), new VisemeMapping("mouthDownLeft", 0.160000f), new VisemeMapping("mouthDownRight", 0.150000f) } },
+ { "RightWrist", new[] { new VisemeMapping("smileRight", 0.040000f), new VisemeMapping("smileLeft", 0.040000f), new VisemeMapping("sneerRight", 0.100000f), new VisemeMapping("sneerLeft", 0.120000f), new VisemeMapping("frownRight", 0.250000f), new VisemeMapping("frownLeft", 0.250000f), new VisemeMapping("jawForward", 0.350000f) } },
+ { "Root", new[] { new VisemeMapping("upperLipCurlIn", 0.330000f), new VisemeMapping("lowerLipCurlIn", 0.420000f), new VisemeMapping("pucker", 0.270000f), new VisemeMapping("jawClench", 0.240000f) } },
+ { "SFX_Volus", new[] { new VisemeMapping("jawClench", 0.010000f) } },
+ { "sneerLeft", new[] { new VisemeMapping("jawOpen", 0.080000f), new VisemeMapping("O_mouth", 0.440000f), new VisemeMapping("jawRotate", 0.210000f) } },
+ { "Stick", new[] { new VisemeMapping("smileLeft", 0.040000f), new VisemeMapping("frownRight", 0.250000f), new VisemeMapping("frownLeft", 0.250000f), new VisemeMapping("upperLipCurlIn", 0.190000f), new VisemeMapping("jawOpen", 0.040000f), new VisemeMapping("jawRotate", 0.160000f), new VisemeMapping("jawClench", 0.280000f) } },
+ { "tongueUp", new[] { new VisemeMapping("smileLeft", 0.040000f), new VisemeMapping("frownRight", 0.250000f), new VisemeMapping("frownLeft", 0.250000f), new VisemeMapping("upperLipCurlIn", 0.190000f), new VisemeMapping("jawOpen", 0.040000f), new VisemeMapping("jawRotate", 0.160000f), new VisemeMapping("jawClench", 0.280000f) } },
+ };
+
+ ///
+ /// All viseme animation names used in lip sync for Volus
+ ///
+ public static readonly string[] VolusVisemes =
+ [
+ "smileRight",
+ "smileLeft",
+ "sneerRight",
+ "sneerLeft",
+ "frownRight",
+ "frownLeft",
+ "jawOpen",
+ "jawRotate",
+ "jawForward",
+ "jawClench",
+ "mouthDownLeft",
+ "mouthDownRight",
+ "O_mouth",
+ "pucker",
+ "upperLipCurlIn",
+ "lowerLipCurlIn",
+ "upperLipCurlOut",
+ "lowerLipCurlOut",
+ "tongueUp"
+ ];
+
+ ///
+ /// Batarian phoneme to viseme mappings - from SFX_Batarian_FaceFX data.
+ /// Note: Batarian use bone and facial feature names as phoneme identifiers in FaceFX mapping.
+ ///
+ public static readonly Dictionary BatarianPhonemeMap = new()
+ {
+ // Batarian phoneme mappings from SFX_Batarian_FaceFX
+ { "brow_left", new[] { new VisemeMapping("jawOpen", 0.040000f), new VisemeMapping("O_mouth", 0.200000f) } },
+ { "brow_right", new[] { new VisemeMapping("jawOpen", 0.005000f) } },
+ { "cheek_left", new[] { new VisemeMapping("smileRight", 0.060000f), new VisemeMapping("smileLeft", 0.060000f), new VisemeMapping("frownRight", 0.594429f), new VisemeMapping("frownLeft", 0.587845f), new VisemeMapping("jawOpen", 0.035714f), new VisemeMapping("sneerLeft", 0.150000f), new VisemeMapping("sneerRight", 0.130000f), new VisemeMapping("jawForward", 0.220000f), new VisemeMapping("jawRotateUp", 0.289409f) } },
+ { "cheek_right", new[] { new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.100000f), new VisemeMapping("O_mouth", 0.541667f), new VisemeMapping("lowerLipUpLeft", 0.143088f), new VisemeMapping("lowerLipUpRight", 0.192446f), new VisemeMapping("sneerLeft", 0.100000f), new VisemeMapping("sneerRight", 0.120000f) } },
+ { "Chest", new[] { new VisemeMapping("frownRight", 0.579785f), new VisemeMapping("frownLeft", 0.579850f), new VisemeMapping("mouthDownLeft", 0.210000f), new VisemeMapping("mouthDownRight", 0.200000f), new VisemeMapping("jawOpen", 0.030000f), new VisemeMapping("sneerLeft", 0.100000f), new VisemeMapping("sneerRight", 0.110000f), new VisemeMapping("jawRotateDown", 0.500000f), new VisemeMapping("tongueUP", 1.000000f) } },
+ { "Chest1", new[] { new VisemeMapping("frownRight", 0.200000f), new VisemeMapping("frownLeft", 0.200000f), new VisemeMapping("jawOpen", 0.050000f) } },
+ { "Chest2", new[] { new VisemeMapping("frownRight", 0.674305f), new VisemeMapping("frownLeft", 0.677114f), new VisemeMapping("sneerLeft", 0.038809f), new VisemeMapping("sneerRight", 0.074519f), new VisemeMapping("jawRotateDown", 0.400000f) } },
+ { "E_GESTURE_NeckBackLeft", new[] { new VisemeMapping("O_mouth", 0.277530f), new VisemeMapping("jawOpen", 0.030000f), new VisemeMapping("pucker", 0.050000f), new VisemeMapping("jawRotateDown", 0.400000f) } },
+ { "E_GESTURE_NeckBackRight", new[] { new VisemeMapping("O_mouth", 0.344494f), new VisemeMapping("jawOpen", 0.030000f) } },
+ { "E_GESTURE_NeckForwardLeft", new[] { new VisemeMapping("frownRight", 0.499908f), new VisemeMapping("frownLeft", 0.498576f), new VisemeMapping("mouthDownLeft", 0.230000f), new VisemeMapping("mouthDownRight", 0.220000f), new VisemeMapping("jawOpen", 0.030000f), new VisemeMapping("jawRotateDown", 0.400000f) } },
+ { "E_GESTURE_NeckForwardRight", new[] { new VisemeMapping("mouthDownLeft", 0.140000f), new VisemeMapping("mouthDownRight", 0.120000f), new VisemeMapping("O_mouth", 0.314732f), new VisemeMapping("jawOpen", 0.030000f), new VisemeMapping("jawRotateDown", 0.400000f) } },
+ { "eye_Left", new[] { new VisemeMapping("frownRight", 0.320186f), new VisemeMapping("frownLeft", 0.322702f), new VisemeMapping("mouthDownLeft", 0.170000f), new VisemeMapping("mouthDownRight", 0.140000f), new VisemeMapping("O_mouth", 0.050595f), new VisemeMapping("jawOpen", 0.030000f), new VisemeMapping("jawRotateDown", 0.400000f) } },
+ { "eye_Left_RX-", new[] { new VisemeMapping("frownRight", 0.342817f), new VisemeMapping("frownLeft", 0.342688f), new VisemeMapping("jawOpen", 0.030000f), new VisemeMapping("lowerLipDownLeft", 0.199389f), new VisemeMapping("lowerLipDownRight", 0.228156f), new VisemeMapping("jawRotateDown", 0.400000f) } },
+ { "eye_Left_RX+", new[] { new VisemeMapping("frownRight", 0.253622f), new VisemeMapping("frownLeft", 0.260081f), new VisemeMapping("mouthDownLeft", 0.180000f), new VisemeMapping("mouthDownRight", 0.190000f), new VisemeMapping("jawOpen", 0.030000f), new VisemeMapping("lowerLipDownLeft", 0.177067f), new VisemeMapping("lowerLipDownRight", 0.172352f), new VisemeMapping("jawRotateDown", 0.400000f), new VisemeMapping("lowerLipCurlOut", 0.270000f) } },
+ { "eye_Right", new[] { new VisemeMapping("frownRight", 0.764832f), new VisemeMapping("frownLeft", 0.766383f), new VisemeMapping("jawOpen", 0.030000f), new VisemeMapping("jawRotateDown", 0.400000f) } },
+ { "eye_Right_RX-", new[] { new VisemeMapping("frownRight", 0.342817f), new VisemeMapping("frownLeft", 0.342688f), new VisemeMapping("jawOpen", 0.030000f), new VisemeMapping("lowerLipDownLeft", 0.199389f), new VisemeMapping("lowerLipDownRight", 0.228156f), new VisemeMapping("jawRotateDown", 0.400000f) } },
+ { "eye_Right_RX+", new[] { new VisemeMapping("frownRight", 0.342817f), new VisemeMapping("frownLeft", 0.342688f), new VisemeMapping("jawOpen", 0.030000f), new VisemeMapping("lowerLipDownLeft", 0.199389f), new VisemeMapping("lowerLipDownRight", 0.228156f), new VisemeMapping("jawRotateDown", 0.400000f) } },
+ { "eyeBlink_Left", new[] { new VisemeMapping("frownRight", 0.320186f), new VisemeMapping("frownLeft", 0.322702f), new VisemeMapping("mouthDownLeft", 0.170000f), new VisemeMapping("mouthDownRight", 0.140000f), new VisemeMapping("O_mouth", 0.050595f), new VisemeMapping("jawOpen", 0.030000f), new VisemeMapping("jawRotateDown", 0.400000f) } },
+ { "eyeBlink_Right", new[] { new VisemeMapping("jawOpen", 0.030000f), new VisemeMapping("sneerLeft", 0.169017f), new VisemeMapping("sneerRight", 0.160084f), new VisemeMapping("jawRotateDown", 0.400000f), new VisemeMapping("lowerLipCurlIn", 0.953105f) } },
+ { "Gaze_Eye_Yaw", new[] { new VisemeMapping("smileRight", 0.050000f), new VisemeMapping("smileLeft", 0.050000f), new VisemeMapping("frownRight", 0.270000f), new VisemeMapping("frownLeft", 0.270000f), new VisemeMapping("mouthDownLeft", 0.220000f), new VisemeMapping("mouthDownRight", 0.210000f), new VisemeMapping("jawOpen", 0.030000f), new VisemeMapping("lowerLipDownLeft", 0.177067f), new VisemeMapping("lowerLipDownRight", 0.153751f), new VisemeMapping("jawRotateDown", 0.400000f) } },
+ { "GOD", new[] { new VisemeMapping("upperLipCurlIn", 0.330000f), new VisemeMapping("lowerLipCurlIn", 0.420000f), new VisemeMapping("pucker", 0.050000f), new VisemeMapping("jawClench", 0.240000f) } },
+ { "Head", new[] { new VisemeMapping("frownRight", 0.436007f), new VisemeMapping("frownLeft", 0.459937f), new VisemeMapping("O_mouth", 0.132440f), new VisemeMapping("jawOpen", 0.060000f), new VisemeMapping("jawRotateDown", 0.400000f), new VisemeMapping("lowerLipCurlIn", 0.260000f) } },
+ { "HeadBase", new[] { new VisemeMapping("jawOpen", 0.040000f), new VisemeMapping("O_mouth", 0.200000f) } },
+ { "innerLowLip_left", new[] { new VisemeMapping("frownRight", 0.378762f), new VisemeMapping("frownLeft", 0.375997f), new VisemeMapping("jawOpen", 0.030000f), new VisemeMapping("sneerLeft", 0.027648f), new VisemeMapping("sneerRight", 0.033596f), new VisemeMapping("jawRotateDown", 0.400000f), new VisemeMapping("tongueUP", 1.000000f) } },
+ { "innerLowLip_right", new[] { new VisemeMapping("jawOpen", 0.040000f), new VisemeMapping("O_mouth", 0.200000f) } },
+ { "innerUpperLip_left", new[] { new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.100000f), new VisemeMapping("O_mouth", 0.277530f), new VisemeMapping("pucker", 0.050000f), new VisemeMapping("lowerLipUpLeft", 0.131927f), new VisemeMapping("lowerLipUpRight", 0.140363f), new VisemeMapping("sneerLeft", 0.070000f), new VisemeMapping("sneerRight", 0.070000f), new VisemeMapping("jawForward", 0.340000f), new VisemeMapping("lowerLipCurlOut", 0.300000f) } },
+ { "innerUpperLip_right", new[] { new VisemeMapping("frownRight", 0.200000f), new VisemeMapping("frownLeft", 0.200000f), new VisemeMapping("jawOpen", 0.050000f) } },
+ { "jawBack", new[] { new VisemeMapping("mouthDownLeft", 0.160000f), new VisemeMapping("mouthDownRight", 0.150000f), new VisemeMapping("O_mouth", 0.100000f), new VisemeMapping("jawOpen", 0.100000f) } },
+ { "jawBone", new[] { new VisemeMapping("frownRight", 0.300000f), new VisemeMapping("frownLeft", 0.300000f), new VisemeMapping("pucker", 0.050000f) } },
+ { "jawRotateDown", new[] { new VisemeMapping("frownRight", 0.505233f), new VisemeMapping("frownLeft", 0.503905f), new VisemeMapping("jawOpen", 0.030000f), new VisemeMapping("jawRotateDown", 0.400000f) } },
+ { "jawRotateUp", new[] { new VisemeMapping("pucker", 0.300000f) } },
+ { "jawSideLeft", new[] { new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.100000f), new VisemeMapping("mouthDownLeft", 0.225446f), new VisemeMapping("mouthDownRight", 0.240327f), new VisemeMapping("jawOpen", 0.100000f), new VisemeMapping("pucker", 0.050000f), new VisemeMapping("jawRotateDown", 0.400000f), new VisemeMapping("lowerLipCurlOut", 0.180000f) } },
+ { "jawSideRight", new[] { new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.100000f), new VisemeMapping("mouthDownLeft", 0.225446f), new VisemeMapping("mouthDownRight", 0.240327f), new VisemeMapping("jawOpen", 0.100000f), new VisemeMapping("pucker", 0.050000f), new VisemeMapping("jawRotateDown", 0.400000f), new VisemeMapping("lowerLipCurlOut", 0.180000f) } },
+ { "LipCorner_left", new[] { new VisemeMapping("smileRight", 0.019052f), new VisemeMapping("smileLeft", 0.020000f), new VisemeMapping("frownRight", 0.445326f), new VisemeMapping("frownLeft", 0.443948f), new VisemeMapping("mouthDownLeft", 0.210000f), new VisemeMapping("mouthDownRight", 0.220000f), new VisemeMapping("jawOpen", 0.030000f), new VisemeMapping("sneerLeft", 0.098333f), new VisemeMapping("sneerRight", 0.096840f), new VisemeMapping("jawRotateDown", 0.400000f) } },
+ { "LipCorner_right", new[] { new VisemeMapping("frownRight", 0.320186f), new VisemeMapping("frownLeft", 0.322702f), new VisemeMapping("mouthDownLeft", 0.170000f), new VisemeMapping("mouthDownRight", 0.140000f), new VisemeMapping("O_mouth", 0.050595f), new VisemeMapping("jawOpen", 0.030000f), new VisemeMapping("jawRotateDown", 0.400000f) } },
+ { "LowerBack", new[] { new VisemeMapping("smileRight", 0.040000f), new VisemeMapping("smileLeft", 0.053577f), new VisemeMapping("frownRight", 0.670312f), new VisemeMapping("frownLeft", 0.675781f), new VisemeMapping("sneerLeft", 0.120000f), new VisemeMapping("sneerRight", 0.100000f), new VisemeMapping("jawForward", 0.350000f), new VisemeMapping("jawRotateUp", 0.337772f) } },
+ { "LowerCheek_left", new[] { new VisemeMapping("frownRight", 0.300000f), new VisemeMapping("frownLeft", 0.300000f), new VisemeMapping("pucker", 0.050000f) } },
+ { "lowerCheek_right", new[] { new VisemeMapping("smileRight", 0.040000f), new VisemeMapping("smileLeft", 0.053577f), new VisemeMapping("frownRight", 0.670312f), new VisemeMapping("frownLeft", 0.675781f), new VisemeMapping("sneerLeft", 0.120000f), new VisemeMapping("sneerRight", 0.100000f), new VisemeMapping("jawForward", 0.350000f), new VisemeMapping("jawRotateUp", 0.337772f) } },
+ { "lowerLip_left", new[] { new VisemeMapping("pucker", 0.300000f) } },
+ { "lowerLip_right", new[] { new VisemeMapping("frownRight", 0.483933f), new VisemeMapping("frownLeft", 0.499908f), new VisemeMapping("jawOpen", 0.030000f), new VisemeMapping("jawRotateDown", 0.400000f) } },
+ { "lowerLipDownLeft", new[] { new VisemeMapping("pucker", 0.300000f) } },
+ { "lowerLipDownRight", new[] { new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.100000f), new VisemeMapping("mouthDownLeft", 0.225446f), new VisemeMapping("mouthDownRight", 0.240327f), new VisemeMapping("jawOpen", 0.100000f), new VisemeMapping("pucker", 0.050000f), new VisemeMapping("jawRotateDown", 0.400000f), new VisemeMapping("lowerLipCurlOut", 0.180000f) } },
+ { "lowLid_Left", new[] { new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.100000f), new VisemeMapping("mouthDownLeft", 0.225446f), new VisemeMapping("mouthDownRight", 0.240327f), new VisemeMapping("jawOpen", 0.100000f), new VisemeMapping("pucker", 0.050000f), new VisemeMapping("jawRotateDown", 0.400000f), new VisemeMapping("lowerLipCurlOut", 0.180000f) } },
+ { "lowLid_Right", new[] { new VisemeMapping("frownRight", 0.320186f), new VisemeMapping("frownLeft", 0.322702f), new VisemeMapping("mouthDownLeft", 0.170000f), new VisemeMapping("mouthDownRight", 0.140000f), new VisemeMapping("O_mouth", 0.050595f), new VisemeMapping("jawOpen", 0.030000f), new VisemeMapping("jawRotateDown", 0.400000f) } },
+ { "MouthBase", new[] { new VisemeMapping("smileRight", 0.036909f), new VisemeMapping("smileLeft", 0.050000f), new VisemeMapping("frownRight", 0.562478f), new VisemeMapping("frownLeft", 0.561197f), new VisemeMapping("sneerLeft", 0.210000f), new VisemeMapping("sneerRight", 0.180000f), new VisemeMapping("jawForward", 0.020000f), new VisemeMapping("jawRotateUp", 0.315451f) } },
+ { "Neck", new[] { new VisemeMapping("upperLipCurlIn", 0.690000f), new VisemeMapping("lowerLipCurlIn", 0.890000f), new VisemeMapping("jawRotateDown", 0.400000f) } },
+ { "Neck1", new[] { new VisemeMapping("O_mouth", 0.470982f), new VisemeMapping("jawOpen", 0.030000f), new VisemeMapping("lowerLipUpLeft", 0.143088f), new VisemeMapping("lowerLipUpRight", 0.144083f), new VisemeMapping("jawRotateDown", 0.400000f) } },
+ { "outBrow_left", new[] { new VisemeMapping("jawOpen", 0.030000f), new VisemeMapping("sneerLeft", 0.169017f), new VisemeMapping("sneerRight", 0.160084f), new VisemeMapping("jawRotateDown", 0.400000f), new VisemeMapping("lowerLipCurlIn", 0.953105f) } },
+ { "outBrow_Right", new[] { new VisemeMapping("jawOpen", 0.030000f), new VisemeMapping("sneerLeft", 0.113214f), new VisemeMapping("sneerRight", 0.130322f), new VisemeMapping("jawRotateDown", 0.400000f), new VisemeMapping("lowerLipCurlIn", 0.770813f) } },
+ { "outerUpperLip_left", new[] { new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.100000f), new VisemeMapping("mouthDownLeft", 0.070000f), new VisemeMapping("mouthDownRight", 0.060000f), new VisemeMapping("O_mouth", 0.337054f), new VisemeMapping("lowerLipUpLeft", 0.105886f), new VisemeMapping("lowerLipUpRight", 0.110601f), new VisemeMapping("sneerLeft", 0.100000f), new VisemeMapping("sneerRight", 0.130000f), new VisemeMapping("jawForward", 0.340000f), new VisemeMapping("lowerLipCurlOut", 0.160000f) } },
+ { "outerUpperLip_right", new[] { new VisemeMapping("frownRight", 0.320186f), new VisemeMapping("frownLeft", 0.322702f), new VisemeMapping("mouthDownLeft", 0.170000f), new VisemeMapping("mouthDownRight", 0.140000f), new VisemeMapping("O_mouth", 0.050595f), new VisemeMapping("jawOpen", 0.030000f), new VisemeMapping("jawRotateDown", 0.400000f) } },
+ { "Root", new[] { new VisemeMapping("upperLipCurlIn", 0.110000f), new VisemeMapping("lowerLipCurlIn", 0.110000f), new VisemeMapping("jawClench", 0.190000f) } },
+ { "SFX_Batarian", new[] { new VisemeMapping("jawClench", 0.010000f) } },
+ { "smileOmouthLeft", new[] { new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.100000f), new VisemeMapping("mouthDownLeft", 0.225446f), new VisemeMapping("mouthDownRight", 0.240327f), new VisemeMapping("jawOpen", 0.100000f), new VisemeMapping("pucker", 0.050000f), new VisemeMapping("jawRotateDown", 0.400000f), new VisemeMapping("lowerLipCurlOut", 0.180000f) } },
+ { "smileOmouthRight", new[] { new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.100000f), new VisemeMapping("mouthDownLeft", 0.225446f), new VisemeMapping("mouthDownRight", 0.240327f), new VisemeMapping("jawOpen", 0.100000f), new VisemeMapping("pucker", 0.050000f), new VisemeMapping("jawRotateDown", 0.400000f), new VisemeMapping("lowerLipCurlOut", 0.180000f) } },
+ { "Sneer", new[] { new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.100000f), new VisemeMapping("mouthDownLeft", 0.225446f), new VisemeMapping("mouthDownRight", 0.240327f), new VisemeMapping("jawOpen", 0.100000f), new VisemeMapping("pucker", 0.050000f), new VisemeMapping("jawRotateDown", 0.400000f), new VisemeMapping("lowerLipCurlOut", 0.180000f) } },
+ { "Tongue", new[] { new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.070000f), new VisemeMapping("O_mouth", 0.344494f), new VisemeMapping("lowerLipUpLeft", 0.076124f), new VisemeMapping("lowerLipUpRight", 0.080839f), new VisemeMapping("sneerLeft", 0.090000f), new VisemeMapping("sneerRight", 0.090000f) } },
+ { "tongueUP", new[] { new VisemeMapping("O_mouth", 0.404018f), new VisemeMapping("jawOpen", 0.030000f), new VisemeMapping("pucker", 0.050000f), new VisemeMapping("lowerLipUpLeft", 0.157969f), new VisemeMapping("lowerLipUpRight", 0.114321f), new VisemeMapping("lowerLipCurlOut", 0.570000f), new VisemeMapping("jawRotateDown", 0.400000f) } },
+ { "underEye_left", new[] { new VisemeMapping("O_mouth", 0.200000f), new VisemeMapping("jawOpen", 0.030000f), new VisemeMapping("jawRotateDown", 0.400000f), new VisemeMapping("tongueUP", 1.000000f) } },
+ { "underEye_Right", new[] { new VisemeMapping("frownRight", 0.171082f), new VisemeMapping("frownLeft", 0.174809f), new VisemeMapping("O_mouth", 0.173363f), new VisemeMapping("jawOpen", 0.061756f), new VisemeMapping("lowerLipDownLeft", 0.132425f), new VisemeMapping("lowerLipDownRight", 0.142590f), new VisemeMapping("jawRotateDown", 0.304290f) } },
+ { "unner_InnerBrowRight_Out", new[] { new VisemeMapping("jawOpen", 0.139881f) } },
+ { "upper_InnerBrowLeft_Out", new[] { new VisemeMapping("jawOpen", 0.139881f) } },
+ { "upperLip_left", new[] { new VisemeMapping("frownRight", 0.200000f), new VisemeMapping("frownLeft", 0.200000f), new VisemeMapping("jawOpen", 0.050000f) } },
+ { "upperLip_right", new[] { new VisemeMapping("frownRight", 0.674305f), new VisemeMapping("frownLeft", 0.677114f), new VisemeMapping("sneerLeft", 0.038809f), new VisemeMapping("sneerRight", 0.074519f), new VisemeMapping("jawRotateDown", 0.400000f) } },
+ { "upperLipCurlOut", new[] { new VisemeMapping("O_mouth", 0.262649f), new VisemeMapping("jawOpen", 0.030000f), new VisemeMapping("jawRotateDown", 0.400000f), new VisemeMapping("upperLipCurlOut", 0.220000f), new VisemeMapping("lowerLipCurlOut", 0.510000f) } },
+ };
+
+ ///
+ /// All viseme animation names used in lip sync for Batarian
+ ///
+ public static readonly string[] BatarianVisemes =
+ [
+ "smileRight",
+ "smileLeft",
+ "frownRight",
+ "frownLeft",
+ "sneerRight",
+ "sneerLeft",
+ "jawOpen",
+ "jawForward",
+ "jawRotateUp",
+ "jawRotateDown",
+ "jawClench",
+ "mouthDownLeft",
+ "mouthDownRight",
+ "O_mouth",
+ "pucker",
+ "upperLipCurlIn",
+ "lowerLipCurlIn",
+ "upperLipCurlOut",
+ "lowerLipCurlOut",
+ "lowerLipUpLeft",
+ "lowerLipUpRight",
+ "lowerLipDownLeft",
+ "lowerLipDownRight",
+ "tongueUP"
+ ];
+
+ ///
+ /// Vorcha phoneme to viseme mappings - from SFX_AlienB_FaceFX data.
+ /// Note: Vorcha use bone and facial feature names as phoneme identifiers in FaceFX mapping.
+ ///
+ public static readonly Dictionary VorchaPhonemeMap = new()
+ {
+ // Vorcha phoneme mappings from SFX_AlienB_FaceFX
+ { "brow_left", new[] { new VisemeMapping("jawRotate", 0.309038f), new VisemeMapping("mouthDownLeft", 0.372206f), new VisemeMapping("mouthDownRight", 0.391642f), new VisemeMapping("O_mouth", 0.799806f), new VisemeMapping("jawOpen", 0.040000f) } },
+ { "brow_right", new[] { new VisemeMapping("jawOpen", 0.010000f) } },
+ { "cheek_left", new[] { new VisemeMapping("smileRight", 0.483965f), new VisemeMapping("smileLeft", 1.000000f), new VisemeMapping("frownRight", 0.192420f), new VisemeMapping("frownLeft", 0.200000f), new VisemeMapping("mouthDownLeft", 0.610301f), new VisemeMapping("mouthDownRight", 0.712342f), new VisemeMapping("sneerLeft", 0.150000f), new VisemeMapping("sneerRight", 0.130000f), new VisemeMapping("jawForward", 0.220000f), new VisemeMapping("noseDown", 0.177843f), new VisemeMapping("smileJawClench", 0.197279f) } },
+ { "cheek_right", new[] { new VisemeMapping("cheekLeft", 0.417208f), new VisemeMapping("cheekRight", 0.215909f), new VisemeMapping("smileRight", 0.702922f), new VisemeMapping("jawClench", 0.326576f), new VisemeMapping("smileLeft", 0.605519f), new VisemeMapping("frownRight", 0.858115f), new VisemeMapping("frownLeft", 0.832792f), new VisemeMapping("mouthDownLeft", 0.425656f), new VisemeMapping("mouthDownRight", 0.454810f), new VisemeMapping("sneerLeft", 0.100000f), new VisemeMapping("sneerRight", 0.120000f), new VisemeMapping("jawForward", 0.255588f), new VisemeMapping("noseUp", 0.241011f) } },
+ { "Chest", new[] { new VisemeMapping("jawRotate", 0.060000f), new VisemeMapping("frownRight", 0.300000f), new VisemeMapping("frownLeft", 0.300000f), new VisemeMapping("mouthDownLeft", 0.210000f), new VisemeMapping("mouthDownRight", 0.200000f), new VisemeMapping("O_mouth", 0.581147f), new VisemeMapping("jawOpen", 0.040000f), new VisemeMapping("sneerLeft", 0.100000f), new VisemeMapping("sneerRight", 0.110000f), new VisemeMapping("tongueUP", 1.000000f) } },
+ { "Chest1", new[] { new VisemeMapping("frownRight", 0.200000f), new VisemeMapping("frownLeft", 0.200000f), new VisemeMapping("jawOpen", 0.050000f) } },
+ { "Chest2", new[] { new VisemeMapping("jawRotate", 0.478470f), new VisemeMapping("frownRight", 0.348214f), new VisemeMapping("frownLeft", 0.340774f), new VisemeMapping("jawOpen", 0.039588f), new VisemeMapping("sneerLeft", 0.087798f), new VisemeMapping("sneerRight", 0.113839f), new VisemeMapping("jawRotateUp", 0.229167f), new VisemeMapping("upperLipCurlIn", 0.330000f), new VisemeMapping("lowerLipCurlIn", 0.046875f) } },
+ { "eye_Left", new[] { new VisemeMapping("jawRotate", 0.347911f), new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.100000f), new VisemeMapping("mouthDownLeft", 0.430515f), new VisemeMapping("mouthDownRight", 0.479106f), new VisemeMapping("O_mouth", 0.372206f), new VisemeMapping("jawOpen", 0.128251f), new VisemeMapping("jawForward", 0.571429f), new VisemeMapping("noseDown", 0.226433f) } },
+ { "eye_Right", new[] { new VisemeMapping("jawRotate", 0.250729f), new VisemeMapping("smileRight", 0.654033f), new VisemeMapping("smileLeft", 0.192420f), new VisemeMapping("frownRight", 0.206997f), new VisemeMapping("frownLeft", 0.454810f), new VisemeMapping("mouthDownLeft", 0.508260f), new VisemeMapping("mouthDownRight", 0.508260f), new VisemeMapping("O_mouth", 0.415938f), new VisemeMapping("sneerLeft", 0.110000f), new VisemeMapping("sneerRight", 0.120000f), new VisemeMapping("jawForward", 0.615160f), new VisemeMapping("noseDown", 0.270165f) } },
+ { "eyeBlink_Left", new[] { new VisemeMapping("jawRotate", 0.347911f), new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.100000f), new VisemeMapping("mouthDownLeft", 0.430515f), new VisemeMapping("mouthDownRight", 0.479106f), new VisemeMapping("O_mouth", 0.372206f), new VisemeMapping("jawOpen", 0.128251f), new VisemeMapping("jawForward", 0.571429f), new VisemeMapping("noseDown", 0.226433f) } },
+ { "eyeBlink_Right", new[] { new VisemeMapping("jawClench", 0.624763f), new VisemeMapping("smileLeft", 0.177843f), new VisemeMapping("O_mouth", 1.000000f), new VisemeMapping("jawOpen", 0.066187f), new VisemeMapping("pucker", 1.000000f), new VisemeMapping("sneerLeft", 0.362013f), new VisemeMapping("sneerRight", 0.394480f), new VisemeMapping("upperLipCurlOut", 0.454811f), new VisemeMapping("lowerLipCurlIn", 1.000000f), new VisemeMapping("noseUp", 0.182702f) } },
+ { "GOD", new[] { new VisemeMapping("jawClench", 1.000000f), new VisemeMapping("frownRight", 0.241011f), new VisemeMapping("frownLeft", 0.221574f), new VisemeMapping("mouthDownLeft", 0.255588f), new VisemeMapping("mouthDownRight", 0.211856f), new VisemeMapping("O_mouth", 1.000000f), new VisemeMapping("jawOpen", 0.066084f), new VisemeMapping("pucker", 1.000000f), new VisemeMapping("jawSideRight", 0.148688f), new VisemeMapping("upperLipCurlIn", 0.330000f), new VisemeMapping("lowerLipCurlIn", 0.420000f) } },
+ { "Head", new[] { new VisemeMapping("frownRight", 0.200000f), new VisemeMapping("frownLeft", 0.200000f), new VisemeMapping("jawOpen", 0.060000f), new VisemeMapping("lowerLipCurlIn", 0.260000f), new VisemeMapping("noseUp", 0.236152f) } },
+ { "HeadBase", new[] { new VisemeMapping("jawRotate", 0.309038f), new VisemeMapping("mouthDownLeft", 0.372206f), new VisemeMapping("mouthDownRight", 0.391642f), new VisemeMapping("O_mouth", 0.799806f), new VisemeMapping("jawOpen", 0.040000f) } },
+ { "innerLowLip_left", new[] { new VisemeMapping("jawRotate", 0.060000f), new VisemeMapping("frownRight", 0.200000f), new VisemeMapping("frownLeft", 0.200000f), new VisemeMapping("jawOpen", 0.040000f), new VisemeMapping("noseDown", 0.192420f), new VisemeMapping("tongueUP", 1.000000f) } },
+ { "innerLowLip_right", new[] { new VisemeMapping("jawRotate", 0.309038f), new VisemeMapping("mouthDownLeft", 0.372206f), new VisemeMapping("mouthDownRight", 0.391642f), new VisemeMapping("O_mouth", 0.799806f), new VisemeMapping("jawOpen", 0.040000f) } },
+ { "innerUpperLip_left", new[] { new VisemeMapping("sneerRight", 0.070000f), new VisemeMapping("sneerLeft", 0.070000f), new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.100000f), new VisemeMapping("lowerLipCurlOut", 0.300000f), new VisemeMapping("jawForward", 0.340000f), new VisemeMapping("pucker", 0.050000f), new VisemeMapping("O_mouth", 0.200000f) } },
+ { "innerUpperLip_right", new[] { new VisemeMapping("frownRight", 0.200000f), new VisemeMapping("frownLeft", 0.200000f), new VisemeMapping("jawOpen", 0.050000f) } },
+ { "jawBone", new[] { new VisemeMapping("jawRotate", 0.190000f), new VisemeMapping("frownRight", 0.300000f), new VisemeMapping("frownLeft", 0.300000f), new VisemeMapping("jawOpen", 0.044021f), new VisemeMapping("pucker", 0.050000f) } },
+ { "LeftCollar", new[] { new VisemeMapping("jawRotate", 0.208048f), new VisemeMapping("smileRight", 0.366815f), new VisemeMapping("cheekPuff", 0.846726f), new VisemeMapping("smileLeft", 0.430059f), new VisemeMapping("frownRight", 0.474702f), new VisemeMapping("frownLeft", 0.489583f), new VisemeMapping("mouthDownLeft", 0.325893f), new VisemeMapping("mouthDownRight", 0.288690f), new VisemeMapping("jawOpen", 0.185882f), new VisemeMapping("pucker", 0.590030f), new VisemeMapping("lowerLipCurlOut", 0.180000f), new VisemeMapping("noseDown", 0.245870f) } },
+ { "LeftElbow", new[] { new VisemeMapping("jawRotate", 0.140000f), new VisemeMapping("O_mouth", 0.300000f), new VisemeMapping("jawOpen", 0.050000f), new VisemeMapping("pucker", 0.050000f), new VisemeMapping("lowerLipCurlOut", 0.570000f), new VisemeMapping("noseUp", 0.216715f) } },
+ { "LeftElbowTwist1", new[] { new VisemeMapping("jawRotate", 0.190000f), new VisemeMapping("frownRight", 0.527697f), new VisemeMapping("frownLeft", 0.483965f), new VisemeMapping("mouthDownLeft", 0.790087f), new VisemeMapping("mouthDownRight", 0.775510f), new VisemeMapping("O_mouth", 0.216715f), new VisemeMapping("jawOpen", 0.079486f), new VisemeMapping("pucker", 0.163265f), new VisemeMapping("jawForward", 0.007775f), new VisemeMapping("noseUp", 0.206997f) } },
+ { "LeftIndexFinger", new[] { new VisemeMapping("jawRotate", 0.284742f), new VisemeMapping("frownRight", 0.454810f), new VisemeMapping("frownLeft", 0.493683f), new VisemeMapping("O_mouth", 0.556851f), new VisemeMapping("jawOpen", 0.066084f), new VisemeMapping("jawForward", 0.168124f), new VisemeMapping("upperLipCurlOut", 0.220000f), new VisemeMapping("lowerLipCurlOut", 0.510000f), new VisemeMapping("noseDown", 0.221575f) } },
+ { "LeftIndexFinger1", new[] { new VisemeMapping("jawRotate", 0.686828f), new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.100000f) } },
+ { "LeftIndexFinger2", new[] { new VisemeMapping("jawRotate", 0.686828f), new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.100000f) } },
+ { "LeftMiddleFinger", new[] { new VisemeMapping("jawRotate", 0.060000f), new VisemeMapping("mouthDownLeft", 0.180000f), new VisemeMapping("mouthDownRight", 0.190000f), new VisemeMapping("jawOpen", 0.247946f), new VisemeMapping("lowerLipCurlOut", 0.270000f), new VisemeMapping("noseDown", 0.138970f) } },
+ { "LeftMiddleFinger1", new[] { new VisemeMapping("jawRotate", 0.474247f), new VisemeMapping("jawClench", 0.280000f), new VisemeMapping("smileLeft", 0.095238f), new VisemeMapping("frownRight", 0.726919f), new VisemeMapping("frownLeft", 0.824101f), new VisemeMapping("O_mouth", 0.265306f), new VisemeMapping("jawOpen", 0.040000f), new VisemeMapping("upperLipCurlIn", 0.190000f), new VisemeMapping("noseDown", 0.197279f) } },
+ { "LeftMiddleFinger2", new[] { new VisemeMapping("jawRotate", 0.474247f), new VisemeMapping("jawClench", 0.280000f), new VisemeMapping("smileLeft", 0.095238f), new VisemeMapping("frownRight", 0.726919f), new VisemeMapping("frownLeft", 0.824101f), new VisemeMapping("O_mouth", 0.265306f), new VisemeMapping("jawOpen", 0.040000f), new VisemeMapping("upperLipCurlIn", 0.190000f), new VisemeMapping("noseDown", 0.197279f) } },
+ { "LeftPinkFinger", new[] { new VisemeMapping("jawRotate", 0.190000f), new VisemeMapping("mouthDownLeft", 0.160000f), new VisemeMapping("mouthDownRight", 0.150000f), new VisemeMapping("O_mouth", 0.100000f), new VisemeMapping("jawOpen", 0.090000f), new VisemeMapping("noseUp", 0.265306f) } },
+ { "LeftPinkFinger1", new[] { new VisemeMapping("jawRotate", 0.200000f), new VisemeMapping("smileRight", 0.040000f), new VisemeMapping("smileLeft", 0.040000f), new VisemeMapping("frownRight", 0.300000f), new VisemeMapping("frownLeft", 0.300000f), new VisemeMapping("mouthDownLeft", 0.230000f), new VisemeMapping("mouthDownRight", 0.210000f), new VisemeMapping("jawOpen", 0.030000f), new VisemeMapping("pucker", 0.736637f), new VisemeMapping("sneerLeft", 0.581147f), new VisemeMapping("sneerRight", 0.571429f), new VisemeMapping("noseDown", 0.270165f) } },
+ { "LeftPinkFinger2", new[] { new VisemeMapping("pucker", 0.300000f) } },
+ { "LeftRingFinger", new[] { new VisemeMapping("jawRotate", 0.474247f), new VisemeMapping("jawClench", 0.280000f), new VisemeMapping("smileLeft", 0.095238f), new VisemeMapping("frownRight", 0.726919f), new VisemeMapping("frownLeft", 0.824101f), new VisemeMapping("O_mouth", 0.265306f), new VisemeMapping("jawOpen", 0.040000f), new VisemeMapping("upperLipCurlIn", 0.190000f), new VisemeMapping("noseDown", 0.197279f) } },
+ { "LeftRingFinger1", new[] { new VisemeMapping("jawRotate", 0.256812f), new VisemeMapping("smileRight", 0.050000f), new VisemeMapping("smileLeft", 0.050000f), new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.100000f), new VisemeMapping("mouthDownLeft", 0.220000f), new VisemeMapping("mouthDownRight", 0.210000f), new VisemeMapping("jawOpen", 0.199181f), new VisemeMapping("noseUp", 0.187561f) } },
+ { "LeftRingFinger2", new[] { new VisemeMapping("jawRotate", 0.240000f), new VisemeMapping("mouthDownLeft", 0.140000f), new VisemeMapping("mouthDownRight", 0.120000f), new VisemeMapping("O_mouth", 0.300000f), new VisemeMapping("jawOpen", 0.181449f), new VisemeMapping("jawForward", 0.547133f) } },
+ { "LeftShoulder", new[] { new VisemeMapping("jawRotate", 0.208048f), new VisemeMapping("smileRight", 0.366815f), new VisemeMapping("cheekPuff", 0.846726f), new VisemeMapping("smileLeft", 0.430059f), new VisemeMapping("frownRight", 0.474702f), new VisemeMapping("frownLeft", 0.489583f), new VisemeMapping("mouthDownLeft", 0.325893f), new VisemeMapping("mouthDownRight", 0.288690f), new VisemeMapping("jawOpen", 0.185882f), new VisemeMapping("pucker", 0.590030f), new VisemeMapping("lowerLipCurlOut", 0.180000f), new VisemeMapping("noseDown", 0.245870f) } },
+ { "LeftThumbFinger", new[] { new VisemeMapping("jawRotate", 0.208048f), new VisemeMapping("smileRight", 0.366815f), new VisemeMapping("cheekPuff", 0.846726f), new VisemeMapping("smileLeft", 0.430059f), new VisemeMapping("frownRight", 0.474702f), new VisemeMapping("frownLeft", 0.489583f), new VisemeMapping("mouthDownLeft", 0.325893f), new VisemeMapping("mouthDownRight", 0.288690f), new VisemeMapping("jawOpen", 0.185882f), new VisemeMapping("pucker", 0.590030f), new VisemeMapping("lowerLipCurlOut", 0.180000f), new VisemeMapping("noseDown", 0.245870f) } },
+ { "LeftThumbFinger1", new[] { new VisemeMapping("jawRotate", 0.208048f), new VisemeMapping("smileRight", 0.366815f), new VisemeMapping("cheekPuff", 0.846726f), new VisemeMapping("smileLeft", 0.430059f), new VisemeMapping("frownRight", 0.474702f), new VisemeMapping("frownLeft", 0.489583f), new VisemeMapping("mouthDownLeft", 0.325893f), new VisemeMapping("mouthDownRight", 0.288690f), new VisemeMapping("jawOpen", 0.185882f), new VisemeMapping("pucker", 0.590030f), new VisemeMapping("lowerLipCurlOut", 0.180000f), new VisemeMapping("noseDown", 0.245870f) } },
+ { "LeftThumbFinger2", new[] { new VisemeMapping("jawRotate", 0.208048f), new VisemeMapping("smileRight", 0.366815f), new VisemeMapping("cheekPuff", 0.846726f), new VisemeMapping("smileLeft", 0.430059f), new VisemeMapping("frownRight", 0.474702f), new VisemeMapping("frownLeft", 0.489583f), new VisemeMapping("mouthDownLeft", 0.325893f), new VisemeMapping("mouthDownRight", 0.288690f), new VisemeMapping("jawOpen", 0.185882f), new VisemeMapping("pucker", 0.590030f), new VisemeMapping("lowerLipCurlOut", 0.180000f), new VisemeMapping("noseDown", 0.245870f) } },
+ { "LeftWrist", new[] { new VisemeMapping("pucker", 0.300000f) } },
+ { "LipCorner_left", new[] { new VisemeMapping("jawRotate", 0.160000f), new VisemeMapping("smileRight", 0.989310f), new VisemeMapping("smileLeft", 1.000000f), new VisemeMapping("frownRight", 0.300000f), new VisemeMapping("frownLeft", 0.300000f), new VisemeMapping("mouthDownLeft", 0.210000f), new VisemeMapping("mouthDownRight", 0.220000f), new VisemeMapping("jawOpen", 0.020000f), new VisemeMapping("sneerLeft", 0.090000f), new VisemeMapping("sneerRight", 0.080000f), new VisemeMapping("noseUp", 0.182702f) } },
+ { "LipCorner_right", new[] { new VisemeMapping("jawRotate", 0.347911f), new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.100000f), new VisemeMapping("mouthDownLeft", 0.430515f), new VisemeMapping("mouthDownRight", 0.479106f), new VisemeMapping("O_mouth", 0.372206f), new VisemeMapping("jawOpen", 0.128251f), new VisemeMapping("jawForward", 0.571429f), new VisemeMapping("noseDown", 0.226433f) } },
+ { "LowerBack", new[] { new VisemeMapping("smileRight", 0.401361f), new VisemeMapping("jawClench", 0.362488f), new VisemeMapping("cheekPuff", 0.508260f), new VisemeMapping("smileLeft", 0.498542f), new VisemeMapping("frownRight", 0.921283f), new VisemeMapping("frownLeft", 0.765792f), new VisemeMapping("sneerLeft", 0.120000f), new VisemeMapping("sneerRight", 0.100000f), new VisemeMapping("jawForward", 0.114674f) } },
+ { "LowerCheek_left", new[] { new VisemeMapping("jawRotate", 0.190000f), new VisemeMapping("frownRight", 0.300000f), new VisemeMapping("frownLeft", 0.300000f), new VisemeMapping("jawOpen", 0.044021f), new VisemeMapping("pucker", 0.050000f) } },
+ { "lowerCheek_right", new[] { new VisemeMapping("smileRight", 0.401361f), new VisemeMapping("jawClench", 0.362488f), new VisemeMapping("cheekPuff", 0.508260f), new VisemeMapping("smileLeft", 0.498542f), new VisemeMapping("frownRight", 0.921283f), new VisemeMapping("frownLeft", 0.765792f), new VisemeMapping("sneerLeft", 0.120000f), new VisemeMapping("sneerRight", 0.100000f), new VisemeMapping("jawForward", 0.114674f) } },
+ { "lowerLip_left", new[] { new VisemeMapping("pucker", 0.300000f) } },
+ { "lowerLip_right", new[] { new VisemeMapping("frownRight", 0.300000f), new VisemeMapping("frownLeft", 0.300000f), new VisemeMapping("jawRotate", 0.100000f) } },
+ { "lowLid_Left", new[] { new VisemeMapping("jawRotate", 0.208048f), new VisemeMapping("smileRight", 0.366815f), new VisemeMapping("cheekPuff", 0.846726f), new VisemeMapping("smileLeft", 0.430059f), new VisemeMapping("frownRight", 0.474702f), new VisemeMapping("frownLeft", 0.489583f), new VisemeMapping("mouthDownLeft", 0.325893f), new VisemeMapping("mouthDownRight", 0.288690f), new VisemeMapping("jawOpen", 0.185882f), new VisemeMapping("pucker", 0.590030f), new VisemeMapping("lowerLipCurlOut", 0.180000f), new VisemeMapping("noseDown", 0.245870f) } },
+ { "lowLid_Right", new[] { new VisemeMapping("jawRotate", 0.347911f), new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.100000f), new VisemeMapping("mouthDownLeft", 0.430515f), new VisemeMapping("mouthDownRight", 0.479106f), new VisemeMapping("O_mouth", 0.372206f), new VisemeMapping("jawOpen", 0.128251f), new VisemeMapping("jawForward", 0.571429f), new VisemeMapping("noseDown", 0.226433f) } },
+ { "MouthBase", new[] { new VisemeMapping("smileRight", 0.050000f), new VisemeMapping("smileLeft", 0.050000f), new VisemeMapping("frownRight", 0.300000f), new VisemeMapping("frownLeft", 0.300000f), new VisemeMapping("sneerLeft", 0.210000f), new VisemeMapping("sneerRight", 0.180000f), new VisemeMapping("jawForward", 0.020000f), new VisemeMapping("noseUp", 0.250729f) } },
+ { "Neck", new[] { new VisemeMapping("cheekLeft", 0.231293f), new VisemeMapping("cheekRight", 0.323615f), new VisemeMapping("smileRight", 0.683188f), new VisemeMapping("jawClench", 0.468082f), new VisemeMapping("smileLeft", 0.658892f), new VisemeMapping("frownRight", 0.289602f), new VisemeMapping("frownLeft", 0.435374f), new VisemeMapping("mouthDownLeft", 0.746356f), new VisemeMapping("mouthDownRight", 0.751215f), new VisemeMapping("O_mouth", 1.000000f), new VisemeMapping("jawForward", 0.406220f), new VisemeMapping("upperLipCurlIn", 0.690000f), new VisemeMapping("lowerLipCurlIn", 0.890000f) } },
+ { "Neck1", new[] { new VisemeMapping("jawRotate", 0.030000f), new VisemeMapping("mouthDownLeft", 0.736637f), new VisemeMapping("mouthDownRight", 0.707483f), new VisemeMapping("O_mouth", 0.100000f), new VisemeMapping("jawForward", 0.265306f), new VisemeMapping("tongueUP", 1.000000f) } },
+ { "outBrow_left", new[] { new VisemeMapping("jawClench", 0.624763f), new VisemeMapping("smileLeft", 0.177843f), new VisemeMapping("O_mouth", 1.000000f), new VisemeMapping("jawOpen", 0.066187f), new VisemeMapping("pucker", 1.000000f), new VisemeMapping("sneerLeft", 0.362013f), new VisemeMapping("sneerRight", 0.394480f), new VisemeMapping("upperLipCurlOut", 0.454811f), new VisemeMapping("lowerLipCurlIn", 1.000000f), new VisemeMapping("noseUp", 0.182702f) } },
+ { "outBrow_Right", new[] { new VisemeMapping("sneerRight", 0.060000f), new VisemeMapping("sneerLeft", 0.040000f), new VisemeMapping("upperLipCurlOut", 0.490000f), new VisemeMapping("lowerLipCurlIn", 1.000000f), new VisemeMapping("jawClench", 0.170000f) } },
+ { "outerUpperLip_left", new[] { new VisemeMapping("sneerRight", 0.130000f), new VisemeMapping("sneerLeft", 0.100000f), new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.100000f), new VisemeMapping("lowerLipCurlOut", 0.160000f), new VisemeMapping("jawForward", 0.340000f), new VisemeMapping("O_mouth", 0.100000f), new VisemeMapping("mouthDownLeft", 0.070000f), new VisemeMapping("mouthDownRight", 0.060000f) } },
+ { "outerUpperLip_right", new[] { new VisemeMapping("jawRotate", 0.347911f), new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.100000f), new VisemeMapping("mouthDownLeft", 0.430515f), new VisemeMapping("mouthDownRight", 0.479106f), new VisemeMapping("O_mouth", 0.372206f), new VisemeMapping("jawOpen", 0.128251f), new VisemeMapping("jawForward", 0.571429f), new VisemeMapping("noseDown", 0.226433f) } },
+ { "Prop02", new[] { new VisemeMapping("jawRotate", 0.160000f), new VisemeMapping("frownRight", 0.200000f), new VisemeMapping("frownLeft", 0.200000f), new VisemeMapping("mouthDownLeft", 0.230000f), new VisemeMapping("mouthDownRight", 0.220000f), new VisemeMapping("jawOpen", 0.066187f), new VisemeMapping("noseUp", 0.206997f) } },
+ { "Root", new[] { new VisemeMapping("upperLipCurlIn", 0.110000f), new VisemeMapping("lowerLipCurlIn", 0.110000f), new VisemeMapping("jawClench", 0.190000f) } },
+ { "SFX_AlienB", new[] { new VisemeMapping("jawClench", 1.000000f), new VisemeMapping("frownRight", 0.688047f), new VisemeMapping("frownLeft", 0.668610f), new VisemeMapping("mouthDownLeft", 0.445092f), new VisemeMapping("mouthDownRight", 0.415938f), new VisemeMapping("pucker", 0.046647f), new VisemeMapping("jawForward", 0.590865f) } },
+ { "Sneer", new[] { new VisemeMapping("jawRotate", 0.208048f), new VisemeMapping("smileRight", 0.366815f), new VisemeMapping("cheekPuff", 0.846726f), new VisemeMapping("smileLeft", 0.430059f), new VisemeMapping("frownRight", 0.474702f), new VisemeMapping("frownLeft", 0.489583f), new VisemeMapping("mouthDownLeft", 0.325893f), new VisemeMapping("mouthDownRight", 0.288690f), new VisemeMapping("jawOpen", 0.185882f), new VisemeMapping("pucker", 0.590030f), new VisemeMapping("lowerLipCurlOut", 0.180000f), new VisemeMapping("noseDown", 0.245870f) } },
+ { "Socket_02", new[] { new VisemeMapping("jawRotate", 0.210000f), new VisemeMapping("frownRight", 0.296131f), new VisemeMapping("frownLeft", 0.296131f), new VisemeMapping("mouthDownLeft", 0.258929f), new VisemeMapping("mouthDownRight", 0.340774f), new VisemeMapping("O_mouth", 1.000000f), new VisemeMapping("jawOpen", 0.173363f), new VisemeMapping("pucker", 0.895089f), new VisemeMapping("noseDown", 0.216715f) } },
+ { "Tongue", new[] { new VisemeMapping("frownRight", 0.100000f), new VisemeMapping("frownLeft", 0.070000f), new VisemeMapping("O_mouth", 0.100000f), new VisemeMapping("pucker", 0.050000f), new VisemeMapping("sneerLeft", 0.090000f), new VisemeMapping("sneerRight", 0.090000f), new VisemeMapping("noseUp", 0.488636f) } },
+ { "underEye_left", new[] { new VisemeMapping("jawRotate", 0.070000f), new VisemeMapping("mouthDownLeft", 0.532556f), new VisemeMapping("mouthDownRight", 0.547133f), new VisemeMapping("O_mouth", 0.200000f), new VisemeMapping("jawOpen", 0.138970f), new VisemeMapping("sneerLeft", 0.435374f), new VisemeMapping("sneerRight", 0.445092f), new VisemeMapping("jawForward", 1.000000f), new VisemeMapping("noseUp", 0.197279f), new VisemeMapping("tongueUP", 1.000000f) } },
+ { "underEye_Right", new[] { new VisemeMapping("jawRotate", 0.070000f), new VisemeMapping("O_mouth", 0.532556f), new VisemeMapping("jawOpen", 0.060000f), new VisemeMapping("pucker", 0.328474f), new VisemeMapping("sneerLeft", 0.571429f), new VisemeMapping("sneerRight", 0.367347f), new VisemeMapping("jawForward", 0.420797f), new VisemeMapping("upperLipCurlOut", 0.430000f), new VisemeMapping("noseUp", 0.109816f), new VisemeMapping("tongueUP", 1.000000f) } },
+ { "upperLip_left", new[] { new VisemeMapping("frownRight", 0.200000f), new VisemeMapping("frownLeft", 0.200000f), new VisemeMapping("jawOpen", 0.050000f) } },
+ { "upperLip_right", new[] { new VisemeMapping("jawRotate", 0.478470f), new VisemeMapping("frownRight", 0.348214f), new VisemeMapping("frownLeft", 0.340774f), new VisemeMapping("jawOpen", 0.039588f), new VisemeMapping("sneerLeft", 0.087798f), new VisemeMapping("sneerRight", 0.113839f), new VisemeMapping("jawRotateUp", 0.229167f), new VisemeMapping("upperLipCurlIn", 0.330000f), new VisemeMapping("lowerLipCurlIn", 0.046875f) } },
+ };
+
+ ///
+ /// All viseme animation names used in lip sync for Vorcha
+ ///
+ public static readonly string[] VorchaVisemes =
+ [
+ "smileRight",
+ "smileLeft",
+ "frownRight",
+ "frownLeft",
+ "sneerRight",
+ "sneerLeft",
+ "jawOpen",
+ "jawRotate",
+ "jawRotateUp",
+ "jawForward",
+ "jawSideRight",
+ "jawClench",
+ "mouthDownLeft",
+ "mouthDownRight",
+ "O_mouth",
+ "pucker",
+ "upperLipCurlIn",
+ "lowerLipCurlIn",
+ "upperLipCurlOut",
+ "lowerLipCurlOut",
+ "cheekLeft",
+ "cheekRight",
+ "cheekPuff",
+ "smileJawClench",
+ "noseUp",
+ "noseDown",
+ "tongueUP"
+ ];
+
+ ///
+ /// Prothean phoneme to viseme mappings - from SFX_Prothean_FaceFX data.
+ /// Note: Prothean uses a unique morph target system with m_ prefixed names.
+ ///
+ public static readonly Dictionary ProtheanPhonemeMap = new()
+ {
+ // Prothean phoneme mappings from SFX_Prothean_FaceFX
+ { "Neck", new[] { new VisemeMapping("neck_RX+", 0.000000f), new VisemeMapping("neck_RX-", 0.000000f), new VisemeMapping("neck_RZ+", 0.000000f), new VisemeMapping("neck_RZ-", 0.000000f), new VisemeMapping("neck_RY+", 0.000000f), new VisemeMapping("neck_RY-", 0.000000f) } },
+ { "Head", new[] { new VisemeMapping("head_RX+", 0.000000f), new VisemeMapping("head_RX-", 0.000000f), new VisemeMapping("head_RY+", 0.000000f), new VisemeMapping("head_RY-", 0.000000f), new VisemeMapping("head_RZ+", 0.000000f), new VisemeMapping("head_RZ-", 0.000000f) } },
+ { "brow_left", new[] { new VisemeMapping("m_CockedBrows_D", 0.574100f), new VisemeMapping("m_EmotionBrows_D", 0.436900f), new VisemeMapping("m_CockedBrows_R", 0.003001f), new VisemeMapping("m_CockedBrows_L", 1.512001f), new VisemeMapping("m_CockedBrows_U", 0.237400f), new VisemeMapping("m_EmotionBrows_R", 0.235501f), new VisemeMapping("m_UpDownBrow_LR", 0.575301f), new VisemeMapping("m_EyelidsLookat_U", 0.000000f), new VisemeMapping("m_EyelidsLookat_D", 0.000001f), new VisemeMapping("m_UpDownBrow_UD", 1.061300f), new VisemeMapping("m_EmotionBrows_L", 0.274302f), new VisemeMapping("m_EmotionBrows_U", 0.650102f) } },
+ { "brow_right", new[] { new VisemeMapping("m_CockedBrows_D", 0.242701f), new VisemeMapping("m_EmotionBrows_D", 0.436900f), new VisemeMapping("m_CockedBrows_R", 1.461500f), new VisemeMapping("m_CockedBrows_L", 0.000001f), new VisemeMapping("m_CockedBrows_U", 0.568701f), new VisemeMapping("m_EmotionBrows_R", 0.235499f), new VisemeMapping("m_UpDownBrow_LR", 0.575301f), new VisemeMapping("m_EyelidsLookat_U", 0.000001f), new VisemeMapping("m_EyelidsLookat_D", 0.000001f), new VisemeMapping("m_UpDownBrow_UD", 1.061300f), new VisemeMapping("m_EmotionBrows_L", 0.274300f), new VisemeMapping("m_EmotionBrows_U", 0.650001f) } },
+ { "eyeBlink_Right", new[] { new VisemeMapping("m_Squint_Eyelids_UD", -0.064899f), new VisemeMapping("m_WideOpen_Eyelids_LR", -0.002100f), new VisemeMapping("m_WideOpen_Eyelids_UD", -0.001801f), new VisemeMapping("m_EyelidsLookat_L", 0.000001f), new VisemeMapping("m_EyelidsLookat_R", 0.000000f), new VisemeMapping("m_EyelidsLookat_U", 0.000000f), new VisemeMapping("m_EyelidsLookat_D", -0.048900f), new VisemeMapping("m_BlinksLookat_UD", -0.064899f), new VisemeMapping("m_BlinksLookat_LR", -0.064899f) } },
+ { "outBrow_left", new[] { new VisemeMapping("m_CockedBrows_D", 1.058300f), new VisemeMapping("m_EmotionBrows_D", 0.135100f), new VisemeMapping("m_CockedBrows_R", 0.000001f), new VisemeMapping("m_CockedBrows_L", 0.000001f), new VisemeMapping("m_CockedBrows_U", -0.005400f), new VisemeMapping("m_EmotionBrows_R", -0.027700f), new VisemeMapping("m_UpDownBrow_LR", 1.058300f), new VisemeMapping("m_EyelidsLookat_U", 0.000000f), new VisemeMapping("m_EyelidsLookat_D", 0.000000f), new VisemeMapping("m_UpDownBrow_UD", -0.000799f), new VisemeMapping("m_EmotionBrows_L", 0.091702f), new VisemeMapping("m_EmotionBrows_U", 1.296301f) } },
+ { "outBrow_Right", new[] { new VisemeMapping("m_CockedBrows_D", -0.000599f), new VisemeMapping("m_EmotionBrows_D", 0.135003f), new VisemeMapping("m_CockedBrows_R", 0.000001f), new VisemeMapping("m_CockedBrows_L", 0.000001f), new VisemeMapping("m_CockedBrows_U", 1.053000f), new VisemeMapping("m_EmotionBrows_R", -0.027699f), new VisemeMapping("m_UpDownBrow_LR", 1.058202f), new VisemeMapping("m_EyelidsLookat_U", 0.000001f), new VisemeMapping("m_EyelidsLookat_D", 0.000000f), new VisemeMapping("m_UpDownBrow_UD", -0.000899f), new VisemeMapping("m_EmotionBrows_L", 0.091701f), new VisemeMapping("m_EmotionBrows_U", 1.296202f) } },
+ { "underEye_left", new[] { new VisemeMapping("m_CockedBrows_D", -0.001900f), new VisemeMapping("m_EmotionBrows_D", -0.001900f), new VisemeMapping("m_CockedBrows_R", -0.001900f), new VisemeMapping("m_CockedBrows_L", -0.001900f), new VisemeMapping("m_CockedBrows_U", -0.001900f), new VisemeMapping("m_EmotionBrows_R", -0.001900f), new VisemeMapping("m_UpDownBrow_LR", -0.001900f), new VisemeMapping("m_UpDownBrow_UD", -0.001900f), new VisemeMapping("m_EmotionBrows_L", -0.001900f), new VisemeMapping("m_EmotionBrows_U", -0.001900f) } },
+ { "underEye_Right", new[] { new VisemeMapping("m_CockedBrows_D", -0.001901f), new VisemeMapping("m_EmotionBrows_D", -0.001901f), new VisemeMapping("m_CockedBrows_R", -0.001901f), new VisemeMapping("m_CockedBrows_L", -0.001901f), new VisemeMapping("m_CockedBrows_U", -0.001901f), new VisemeMapping("m_EmotionBrows_R", -0.001901f), new VisemeMapping("m_UpDownBrow_LR", -0.001901f), new VisemeMapping("m_UpDownBrow_UD", -0.001901f), new VisemeMapping("m_EmotionBrows_L", -0.001901f), new VisemeMapping("m_EmotionBrows_U", -0.001901f) } },
+ { "jawBone", new[] { new VisemeMapping("m_Open_LR", 0.035004f), new VisemeMapping("m_Open_UD", 0.035004f), new VisemeMapping("m_JawRotate_U", 0.266388f), new VisemeMapping("m_JawRotate_D", 0.036392f), new VisemeMapping("m_JawRotate_L", 0.035004f), new VisemeMapping("m_Jaw-", 0.002396f), new VisemeMapping("m_Jaw+", 0.000397f), new VisemeMapping("m_JawRotate_R", 0.035004f), new VisemeMapping("m_Angry_UD", -0.241714f) } },
+ { "LowerCheek_left", new[] { new VisemeMapping("m_Open_UD", 2.313103f), new VisemeMapping("m_OH", 0.126709f), new VisemeMapping("m_EE", 0.002609f), new VisemeMapping("m_EH", 0.001907f), new VisemeMapping("m_OW", 0.000000f), new VisemeMapping("m_ZZ", 0.001907f), new VisemeMapping("m_TH", 0.000595f), new VisemeMapping("m_N", 0.000595f), new VisemeMapping("m_L", 0.000595f), new VisemeMapping("m_G", 0.000595f), new VisemeMapping("m_Open", 0.000595f), new VisemeMapping("m_Closed_R", 0.033004f), new VisemeMapping("m_Offset_L", 0.034515f), new VisemeMapping("m_Closed_U", 0.034012f), new VisemeMapping("m_Offset_R", 0.034515f), new VisemeMapping("m_Offset_U", -0.741989f), new VisemeMapping("m_Closed_D", -0.119888f), new VisemeMapping("m_Offset_D", 0.034515f), new VisemeMapping("m_M", 0.000595f), new VisemeMapping("m_FV", 0.000595f), new VisemeMapping("m_Angry_UD", 1.356705f), new VisemeMapping("m_Smile_Frown_U", -3.582093f), new VisemeMapping("m_Smile_Frown_D", 1.648514f), new VisemeMapping("m_Smile_Frown_L", -3.575791f), new VisemeMapping("m_Smile_Frown_R", -1.511002f), new VisemeMapping("m_Closed_L", 0.035507f), new VisemeMapping("m_Angry_L", 0.035507f), new VisemeMapping("m_Angry_R", 0.035507f) } },
+ { "lowerCheek_right", new[] { new VisemeMapping("m_Open_UD", 1.860199f), new VisemeMapping("m_OH", 0.126801f), new VisemeMapping("m_EE", 0.002686f), new VisemeMapping("m_EH", 0.001999f), new VisemeMapping("m_OW", -0.000015f), new VisemeMapping("m_ZZ", 0.001984f), new VisemeMapping("m_TH", 0.000687f), new VisemeMapping("m_N", 0.000687f), new VisemeMapping("m_L", 0.000687f), new VisemeMapping("m_G", 0.000687f), new VisemeMapping("m_Open", 0.000687f), new VisemeMapping("m_Closed_R", 0.035996f), new VisemeMapping("m_Offset_L", 0.036499f), new VisemeMapping("m_Closed_U", 0.034988f), new VisemeMapping("m_Offset_R", 0.036499f), new VisemeMapping("m_Offset_U", 0.036499f), new VisemeMapping("m_Closed_D", -0.119202f), new VisemeMapping("m_Offset_D", 0.036499f), new VisemeMapping("m_M", 0.000687f), new VisemeMapping("m_FV", 0.000687f), new VisemeMapping("m_Angry_UD", 1.672989f), new VisemeMapping("m_Smile_Frown_U", -3.581100f), new VisemeMapping("m_Smile_Frown_D", 1.648499f), new VisemeMapping("m_Smile_Frown_L", -1.672012f), new VisemeMapping("m_Smile_Frown_R", -3.887787f), new VisemeMapping("m_Closed_L", 0.035492f), new VisemeMapping("m_Angry_L", 0.035492f), new VisemeMapping("m_Angry_R", 0.035492f) } },
+ { "Tongue", new[] { new VisemeMapping("m_TH", -1.315598f), new VisemeMapping("m_N", -1.209579f), new VisemeMapping("m_L", -1.423691f), new VisemeMapping("m_Offset_L", 0.035492f), new VisemeMapping("m_Offset_R", 0.035492f), new VisemeMapping("m_Offset_U", 0.035492f), new VisemeMapping("m_Offset_D", 0.035492f), new VisemeMapping("m_Smile_Frown_U", 0.035492f), new VisemeMapping("m_Smile_Frown_D", 0.034500f), new VisemeMapping("m_Smile_Frown_L", 0.035492f), new VisemeMapping("m_Smile_Frown_R", 0.035492f), new VisemeMapping("m_Angry_L", 0.035492f), new VisemeMapping("m_Angry_R", 0.035492f) } },
+ { "lowerLip_right", new[] { new VisemeMapping("m_Open_LR", 0.291214f), new VisemeMapping("m_Open_UD", 0.399506f), new VisemeMapping("m_OH", 0.224289f), new VisemeMapping("m_EE", 0.336899f), new VisemeMapping("m_EH", 0.281296f), new VisemeMapping("m_OW", 0.403000f), new VisemeMapping("m_ZZ", 0.201706f), new VisemeMapping("m_TH", 0.113693f), new VisemeMapping("m_N", 0.235306f), new VisemeMapping("m_L", 0.235306f), new VisemeMapping("m_G", 0.339691f), new VisemeMapping("m_Open", 0.436096f), new VisemeMapping("m_Closed_R", 0.122391f), new VisemeMapping("m_Offset_L", 0.035004f), new VisemeMapping("m_Closed_U", -0.186401f), new VisemeMapping("m_Offset_R", 0.034988f), new VisemeMapping("m_Offset_U", -1.028503f), new VisemeMapping("m_Closed_D", -0.335098f), new VisemeMapping("m_Offset_D", 1.140991f), new VisemeMapping("m_M", -0.118500f), new VisemeMapping("m_FV", -0.381393f), new VisemeMapping("m_Angry_UD", 1.507019f), new VisemeMapping("m_Smile_Frown_U", -0.095688f), new VisemeMapping("m_Smile_Frown_D", -0.167587f), new VisemeMapping("m_Smile_Frown_L", -0.884308f), new VisemeMapping("m_Smile_Frown_R", -0.131500f), new VisemeMapping("m_Closed_L", -0.031494f), new VisemeMapping("m_Angry_L", 0.143005f), new VisemeMapping("m_Angry_R", 0.035004f) } },
+ { "lowerLip_left", new[] { new VisemeMapping("m_Open_LR", 0.291214f), new VisemeMapping("m_Open_UD", 0.400711f), new VisemeMapping("m_OH", 0.226303f), new VisemeMapping("m_EE", 0.342789f), new VisemeMapping("m_EH", 0.281204f), new VisemeMapping("m_OW", 0.402893f), new VisemeMapping("m_ZZ", 0.201599f), new VisemeMapping("m_TH", 0.113602f), new VisemeMapping("m_N", 0.235107f), new VisemeMapping("m_L", 0.235107f), new VisemeMapping("m_G", 0.339600f), new VisemeMapping("m_Open", 0.436005f), new VisemeMapping("m_Closed_R", 0.110413f), new VisemeMapping("m_Offset_L", 0.035995f), new VisemeMapping("m_Closed_U", -0.151398f), new VisemeMapping("m_Offset_R", 0.035995f), new VisemeMapping("m_Offset_U", -1.027496f), new VisemeMapping("m_Closed_D", -0.334793f), new VisemeMapping("m_Offset_D", 1.140991f), new VisemeMapping("m_M", -0.106598f), new VisemeMapping("m_FV", -0.381500f), new VisemeMapping("m_Angry_UD", 1.572006f), new VisemeMapping("m_Smile_Frown_U", -0.142700f), new VisemeMapping("m_Smile_Frown_D", -0.215607f), new VisemeMapping("m_Smile_Frown_L", -0.131500f), new VisemeMapping("m_Smile_Frown_R", -0.884293f), new VisemeMapping("m_Closed_L", -0.009598f), new VisemeMapping("m_Angry_L", 0.035995f), new VisemeMapping("m_Angry_R", 0.143005f) } },
+ { "LipCorner_right", new[] { new VisemeMapping("m_Open_UD", 1.258209f), new VisemeMapping("m_OH", -0.434387f), new VisemeMapping("m_EE", -0.357086f), new VisemeMapping("m_EH", -0.405090f), new VisemeMapping("m_OW", -0.465195f), new VisemeMapping("m_ZZ", -0.388397f), new VisemeMapping("m_TH", -0.566788f), new VisemeMapping("m_N", -0.567108f), new VisemeMapping("m_L", -0.567108f), new VisemeMapping("m_G", -0.566788f), new VisemeMapping("m_Open", -0.410690f), new VisemeMapping("m_Flap", -0.222992f), new VisemeMapping("m_JawRotate_L", -0.041901f), new VisemeMapping("m_Closed_R", -0.297592f), new VisemeMapping("m_Offset_L", 0.035507f), new VisemeMapping("m_JawRotate_R", -0.041702f), new VisemeMapping("m_Closed_U", -0.459885f), new VisemeMapping("m_Offset_R", 0.036499f), new VisemeMapping("m_Offset_U", -1.026993f), new VisemeMapping("m_Closed_D", -0.848495f), new VisemeMapping("m_Offset_D", 1.141495f), new VisemeMapping("m_M", -0.451904f), new VisemeMapping("m_FV", -0.761795f), new VisemeMapping("m_Angry_UD", -0.280289f), new VisemeMapping("m_Smile_Frown_U", -2.468109f), new VisemeMapping("m_Smile_Frown_D", 0.592896f), new VisemeMapping("m_Smile_Frown_L", -1.457992f), new VisemeMapping("m_Smile_Frown_R", -2.873810f), new VisemeMapping("m_Closed_L", -0.728989f), new VisemeMapping("m_Angry_L", 0.035492f), new VisemeMapping("m_Angry_R", -0.343994f) } },
+ { "LipCorner_left", new[] { new VisemeMapping("m_Open_UD", 0.859612f), new VisemeMapping("m_OH", -0.389404f), new VisemeMapping("m_EE", -0.357010f), new VisemeMapping("m_EH", -0.405106f), new VisemeMapping("m_OW", -0.465103f), new VisemeMapping("m_ZZ", -0.388397f), new VisemeMapping("m_TH", -0.566803f), new VisemeMapping("m_N", -0.567001f), new VisemeMapping("m_L", -0.567001f), new VisemeMapping("m_G", -0.566803f), new VisemeMapping("m_Open", -0.410690f), new VisemeMapping("m_Flap", -0.223007f), new VisemeMapping("m_Closed_R", -0.400589f), new VisemeMapping("m_Offset_L", 0.034500f), new VisemeMapping("m_Closed_U", -0.421890f), new VisemeMapping("m_Offset_R", 0.035492f), new VisemeMapping("m_Offset_U", -1.028000f), new VisemeMapping("m_Closed_D", -0.848984f), new VisemeMapping("m_Offset_D", 1.140488f), new VisemeMapping("m_M", -0.463898f), new VisemeMapping("m_FV", -0.762802f), new VisemeMapping("m_Angry_UD", -0.380188f), new VisemeMapping("m_Smile_Frown_U", -2.468201f), new VisemeMapping("m_Smile_Frown_D", 0.592896f), new VisemeMapping("m_Smile_Frown_L", -2.873795f), new VisemeMapping("m_Smile_Frown_R", -1.458008f), new VisemeMapping("m_Closed_L", -0.707993f), new VisemeMapping("m_Angry_L", -0.343994f), new VisemeMapping("m_Angry_R", 0.035492f) } },
+ { "throat_left", new[] { new VisemeMapping("m_Open_LR", 4.224503f), new VisemeMapping("m_Open_UD", 5.796401f), new VisemeMapping("m_Closed_R", 3.381607f), new VisemeMapping("m_Closed_L", 0.035492f) } },
+ { "throat_right", new[] { new VisemeMapping("m_Open_LR", 4.224503f), new VisemeMapping("m_Open_UD", 5.796212f), new VisemeMapping("m_Closed_R", 3.383514f), new VisemeMapping("m_Closed_L", 0.036514f) } },
+ { "lowerLip_mid", new[] { new VisemeMapping("m_Open_LR", 0.620193f), new VisemeMapping("m_Open_UD", 0.467598f), new VisemeMapping("m_OH", 0.792191f), new VisemeMapping("m_EE", 0.653702f), new VisemeMapping("m_EH", 0.519501f), new VisemeMapping("m_OW", 0.614090f), new VisemeMapping("m_ZZ", 0.591583f), new VisemeMapping("m_TH", 0.240891f), new VisemeMapping("m_N", 0.540298f), new VisemeMapping("m_L", 0.406296f), new VisemeMapping("m_G", 0.548492f), new VisemeMapping("m_Open", 0.695190f), new VisemeMapping("m_Flap", 0.099792f), new VisemeMapping("m_Closed_R", -0.607803f), new VisemeMapping("m_Offset_L", 0.035294f), new VisemeMapping("m_Closed_U", -0.267197f), new VisemeMapping("m_Offset_R", 0.035294f), new VisemeMapping("m_Offset_U", -1.028198f), new VisemeMapping("m_Closed_D", -0.236801f), new VisemeMapping("m_Offset_D", 1.141296f), new VisemeMapping("m_M", 0.003296f), new VisemeMapping("m_FV", -0.209595f), new VisemeMapping("m_Angry_UD", 0.774200f), new VisemeMapping("m_Smile_Frown_U", 0.336594f), new VisemeMapping("m_Smile_Frown_D", -0.287109f), new VisemeMapping("m_Smile_Frown_L", -0.394897f), new VisemeMapping("m_Smile_Frown_R", -0.394897f), new VisemeMapping("m_Closed_L", 0.035187f), new VisemeMapping("m_Angry_L", 0.096298f), new VisemeMapping("m_Angry_R", 0.096298f) } },
+ { "upperLip_left", new[] { new VisemeMapping("m_Open_LR", 0.035400f), new VisemeMapping("m_Open_UD", 0.268707f), new VisemeMapping("m_OH", 0.513092f), new VisemeMapping("m_EE", -0.028610f), new VisemeMapping("m_EH", 0.114594f), new VisemeMapping("m_OW", 0.001099f), new VisemeMapping("m_ZZ", -0.043900f), new VisemeMapping("m_TH", 0.102493f), new VisemeMapping("m_N", -0.171097f), new VisemeMapping("m_L", -0.035110f), new VisemeMapping("m_G", 0.102600f), new VisemeMapping("m_Open", 0.375793f), new VisemeMapping("m_Flap", 0.057098f), new VisemeMapping("m_JawRotate_L", -0.267288f), new VisemeMapping("m_Closed_R", 0.175400f), new VisemeMapping("m_Offset_L", 0.033401f), new VisemeMapping("m_JawRotate_R", 0.371292f), new VisemeMapping("m_Closed_U", -0.027802f), new VisemeMapping("m_Offset_R", 0.134399f), new VisemeMapping("m_Offset_U", -1.029099f), new VisemeMapping("m_Closed_D", 0.178207f), new VisemeMapping("m_Offset_D", 1.453400f), new VisemeMapping("m_M", -0.070206f), new VisemeMapping("m_FV", -0.102097f), new VisemeMapping("m_Angry_UD", -1.807404f), new VisemeMapping("m_Smile_Frown_U", -0.492584f), new VisemeMapping("m_Smile_Frown_D", 0.244110f), new VisemeMapping("m_Smile_Frown_L", -0.655396f), new VisemeMapping("m_Smile_Frown_R", -0.086090f), new VisemeMapping("m_Closed_L", -0.032196f), new VisemeMapping("m_Angry_L", -1.330200f), new VisemeMapping("m_Angry_R", 0.035400f) } },
+ { "Sneer", new[] { new VisemeMapping("m_Open_LR", 0.035110f), new VisemeMapping("m_Open_UD", -0.114197f), new VisemeMapping("m_OH", 0.078903f), new VisemeMapping("m_EE", -0.106201f), new VisemeMapping("m_EH", 0.001297f), new VisemeMapping("m_OW", -0.197800f), new VisemeMapping("m_ZZ", -0.150299f), new VisemeMapping("m_TH", -0.117386f), new VisemeMapping("m_N", -0.228485f), new VisemeMapping("m_L", -0.092499f), new VisemeMapping("m_G", -0.036499f), new VisemeMapping("m_Open", 0.001297f), new VisemeMapping("m_Flap", 0.040192f), new VisemeMapping("m_JawRotate_L", -0.169296f), new VisemeMapping("m_Closed_R", -0.026489f), new VisemeMapping("m_Offset_L", 0.034103f), new VisemeMapping("m_JawRotate_R", -0.169495f), new VisemeMapping("m_Closed_U", -0.230286f), new VisemeMapping("m_Offset_R", 0.034103f), new VisemeMapping("m_Offset_U", -1.028381f), new VisemeMapping("m_Closed_D", 0.037308f), new VisemeMapping("m_Offset_D", 1.140106f), new VisemeMapping("m_M", -0.071594f), new VisemeMapping("m_FV", -0.394302f), new VisemeMapping("m_Angry_UD", -1.009003f), new VisemeMapping("m_Smile_Frown_U", -0.338196f), new VisemeMapping("m_Smile_Frown_D", -0.478699f), new VisemeMapping("m_Smile_Frown_L", -0.145294f), new VisemeMapping("m_Smile_Frown_R", -0.145294f), new VisemeMapping("m_Closed_L", 0.185104f), new VisemeMapping("m_Angry_L", -0.110291f), new VisemeMapping("m_Angry_R", -0.110382f) } },
+ { "upperLip_right", new[] { new VisemeMapping("m_Open_LR", 0.035507f), new VisemeMapping("m_Open_UD", 0.268112f), new VisemeMapping("m_OH", 0.460495f), new VisemeMapping("m_EE", -0.028595f), new VisemeMapping("m_EH", 0.114700f), new VisemeMapping("m_OW", 0.001205f), new VisemeMapping("m_ZZ", -0.043793f), new VisemeMapping("m_TH", 0.031815f), new VisemeMapping("m_N", -0.170990f), new VisemeMapping("m_L", -0.034988f), new VisemeMapping("m_G", -0.015900f), new VisemeMapping("m_Open", 0.375916f), new VisemeMapping("m_Flap", 0.057007f), new VisemeMapping("m_JawRotate_L", 0.371704f), new VisemeMapping("m_Closed_R", 0.178802f), new VisemeMapping("m_Offset_L", 0.134506f), new VisemeMapping("m_JawRotate_R", -0.267288f), new VisemeMapping("m_Closed_U", -0.010193f), new VisemeMapping("m_Offset_R", 0.035507f), new VisemeMapping("m_Offset_U", -1.028992f), new VisemeMapping("m_Closed_D", 0.178604f), new VisemeMapping("m_Offset_D", 1.453506f), new VisemeMapping("m_M", -0.071198f), new VisemeMapping("m_FV", -0.101990f), new VisemeMapping("m_Angry_UD", -1.979095f), new VisemeMapping("m_Smile_Frown_U", -0.458588f), new VisemeMapping("m_Smile_Frown_D", 0.262405f), new VisemeMapping("m_Smile_Frown_L", -0.085999f), new VisemeMapping("m_Smile_Frown_R", -0.655396f), new VisemeMapping("m_Closed_L", -0.011093f), new VisemeMapping("m_Angry_L", 0.035507f), new VisemeMapping("m_Angry_R", -1.167786f) } },
+ { "eye_Right", new[] { new VisemeMapping("eye_Right_RX+", 0.000000f), new VisemeMapping("eye_Right_RX-", 0.000000f), new VisemeMapping("eye_Right_RY+", 0.000000f), new VisemeMapping("eye_Right_RY-", 0.000000f), new VisemeMapping("eye_Right_RZ+", 0.000000f), new VisemeMapping("eye_Right_RZ-", 0.000000f) } },
+ { "eye_Left", new[] { new VisemeMapping("eye_Left_RX+", 0.000000f), new VisemeMapping("eye_Left_RX-", 0.000000f), new VisemeMapping("eye_Left_RY+", 0.000000f), new VisemeMapping("eye_Left_RY-", 0.000000f), new VisemeMapping("eye_Left_RZ+", 0.000000f), new VisemeMapping("eye_Left_RZ-", 0.000000f) } },
+ { "lowLid_Right", new[] { new VisemeMapping("m_Squint_Eyelids_UD", -0.077099f), new VisemeMapping("m_WideOpen_Eyelids_LR", -0.010899f), new VisemeMapping("m_WideOpen_Eyelids_UD", -0.001900f), new VisemeMapping("m_EyelidsLookat_L", 0.000001f), new VisemeMapping("m_EyelidsLookat_R", 0.000000f), new VisemeMapping("m_EyelidsLookat_U", 0.000000f), new VisemeMapping("m_EyelidsLookat_D", 0.000000f), new VisemeMapping("m_BlinksLookat_UD", -0.017000f), new VisemeMapping("m_BlinksLookat_LR", -0.137198f) } },
+ { "eyeBlink_Left", new[] { new VisemeMapping("m_Squint_Eyelids_LR", -0.064899f), new VisemeMapping("m_WideOpen_Eyelids_LR", -0.002100f), new VisemeMapping("m_WideOpen_Eyelids_UD", -0.001800f), new VisemeMapping("m_EyelidsLookat_L", 0.000001f), new VisemeMapping("m_EyelidsLookat_R", 0.000000f), new VisemeMapping("m_EyelidsLookat_U", 0.000001f), new VisemeMapping("m_EyelidsLookat_D", -0.048900f), new VisemeMapping("m_BlinksLookat_UD", -0.064899f), new VisemeMapping("m_BlinksLookat_LR", -0.064899f) } },
+ { "lowLid_Left", new[] { new VisemeMapping("m_Squint_Eyelids_LR", -0.077100f), new VisemeMapping("m_WideOpen_Eyelids_LR", -0.011000f), new VisemeMapping("m_WideOpen_Eyelids_UD", -0.001800f), new VisemeMapping("m_EyelidsLookat_L", 0.000001f), new VisemeMapping("m_EyelidsLookat_R", 0.000000f), new VisemeMapping("m_EyelidsLookat_U", 0.000001f), new VisemeMapping("m_EyelidsLookat_D", 0.000001f), new VisemeMapping("m_BlinksLookat_UD", -0.017000f), new VisemeMapping("m_BlinksLookat_LR", -0.137199f) } },
+ { "nose", new[] { new VisemeMapping("m_JawRotate_L", -0.005198f), new VisemeMapping("m_Closed_R", -0.005198f), new VisemeMapping("m_JawRotate_R", -0.005198f), new VisemeMapping("m_Angry_UD", -0.005500f), new VisemeMapping("m_Closed_L", -0.005198f), new VisemeMapping("m_Angry_L", -0.005400f), new VisemeMapping("m_Angry_R", -0.005400f) } },
+ { "cheek_right", new[] { new VisemeMapping("m_Open_UD", 0.829101f), new VisemeMapping("m_JawRotate_L", 0.815900f), new VisemeMapping("m_Closed_R", 2.052300f), new VisemeMapping("m_JawRotate_R", -0.004500f), new VisemeMapping("m_Closed_U", -0.004300f), new VisemeMapping("m_Closed_D", -0.001900f), new VisemeMapping("m_Angry_UD", -0.004800f), new VisemeMapping("m_Sneer_UD", -0.007999f), new VisemeMapping("m_Smile_Frown_U", -0.004500f), new VisemeMapping("m_Smile_Frown_D", -0.004300f), new VisemeMapping("m_Smile_Frown_L", -0.004300f), new VisemeMapping("m_Smile_Frown_R", -0.004700f), new VisemeMapping("m_Closed_L", -0.004000f), new VisemeMapping("m_Angry_L", -0.004400f), new VisemeMapping("m_Angry_R", -0.004800f) } },
+ { "cheek_left", new[] { new VisemeMapping("m_Open_UD", -0.004300f), new VisemeMapping("m_JawRotate_L", -0.004399f), new VisemeMapping("m_Closed_R", 2.328001f), new VisemeMapping("m_JawRotate_R", 0.955801f), new VisemeMapping("m_Closed_U", -0.004299f), new VisemeMapping("m_Closed_D", -0.001800f), new VisemeMapping("m_Sneer_LR", -0.007900f), new VisemeMapping("m_Angry_UD", -0.004800f), new VisemeMapping("m_Smile_Frown_U", -0.004500f), new VisemeMapping("m_Smile_Frown_D", -0.004300f), new VisemeMapping("m_Smile_Frown_L", -0.004499f), new VisemeMapping("m_Smile_Frown_R", -0.004200f), new VisemeMapping("m_Closed_L", -0.004200f), new VisemeMapping("m_Angry_L", -0.004700f), new VisemeMapping("m_Angry_R", -0.004300f) } },
+ { "upperBrow_Left", new[] { new VisemeMapping("m_CockedBrows_D", 0.605400f), new VisemeMapping("m_EmotionBrows_D", 0.015800f), new VisemeMapping("m_CockedBrows_R", 0.002299f), new VisemeMapping("m_CockedBrows_L", 0.000001f), new VisemeMapping("m_CockedBrows_U", -0.003800f), new VisemeMapping("m_EmotionBrows_R", -0.246000f), new VisemeMapping("m_UpDownBrow_LR", 0.605400f), new VisemeMapping("m_EyelidsLookat_L", 0.000000f), new VisemeMapping("m_EyelidsLookat_U", 0.000000f), new VisemeMapping("m_EyelidsLookat_D", 0.000000f), new VisemeMapping("m_UpDownBrow_UD", -0.001101f), new VisemeMapping("m_EmotionBrows_L", 0.029799f), new VisemeMapping("m_EmotionBrows_U", -0.082499f) } },
+ { "upperBrow_Right", new[] { new VisemeMapping("m_CockedBrows_D", 0.000000f), new VisemeMapping("m_EmotionBrows_D", 0.015700f), new VisemeMapping("m_CockedBrows_R", 0.002100f), new VisemeMapping("m_CockedBrows_L", 0.000001f), new VisemeMapping("m_CockedBrows_U", 0.600600f), new VisemeMapping("m_EmotionBrows_R", -0.245900f), new VisemeMapping("m_UpDownBrow_LR", 0.605200f), new VisemeMapping("m_EyelidsLookat_U", 0.000000f), new VisemeMapping("m_EyelidsLookat_D", 0.000001f), new VisemeMapping("m_UpDownBrow_UD", -0.001100f), new VisemeMapping("m_EmotionBrows_L", 0.029800f), new VisemeMapping("m_EmotionBrows_U", -0.082601f) } },
+ { "lowLid_top_Left", new[] { new VisemeMapping("m_Squint_Eyelids_LR", -0.014700f), new VisemeMapping("m_WideOpen_Eyelids_LR", -0.002200f), new VisemeMapping("m_WideOpen_Eyelids_UD", -0.001900f), new VisemeMapping("m_EyelidsLookat_L", 0.000000f), new VisemeMapping("m_EyelidsLookat_R", 0.000000f), new VisemeMapping("m_EyelidsLookat_U", 0.000000f), new VisemeMapping("m_EyelidsLookat_D", 0.000000f), new VisemeMapping("m_BlinksLookat_UD", -0.014699f), new VisemeMapping("m_BlinksLookat_LR", -0.014700f) } },
+ { "eyeBlink_top_Left", new[] { new VisemeMapping("m_Squint_Eyelids_LR", 0.017900f), new VisemeMapping("m_WideOpen_Eyelids_LR", -0.035900f), new VisemeMapping("m_WideOpen_Eyelids_UD", -0.036799f), new VisemeMapping("m_EyelidsLookat_L", -0.034900f), new VisemeMapping("m_EyelidsLookat_R", 0.000000f), new VisemeMapping("m_EyelidsLookat_U", -0.034900f), new VisemeMapping("m_EyelidsLookat_D", -0.034900f), new VisemeMapping("m_BlinksLookat_UD", 0.017800f), new VisemeMapping("m_BlinksLookat_LR", 0.018300f) } },
+ { "lowLid_top_Right", new[] { new VisemeMapping("m_Squint_Eyelids_UD", -0.014700f), new VisemeMapping("m_WideOpen_Eyelids_LR", -0.002200f), new VisemeMapping("m_WideOpen_Eyelids_UD", -0.001900f), new VisemeMapping("m_EyelidsLookat_L", -0.000001f), new VisemeMapping("m_EyelidsLookat_R", -0.000001f), new VisemeMapping("m_EyelidsLookat_U", 0.000000f), new VisemeMapping("m_EyelidsLookat_D", 0.000000f), new VisemeMapping("m_BlinksLookat_UD", -0.014701f), new VisemeMapping("m_BlinksLookat_LR", -0.014700f) } },
+ { "eyeBlink_top_Right", new[] { new VisemeMapping("m_Squint_Eyelids_UD", 0.052799f), new VisemeMapping("m_WideOpen_Eyelids_LR", -0.001000f), new VisemeMapping("m_WideOpen_Eyelids_UD", -0.001900f), new VisemeMapping("m_EyelidsLookat_L", 0.033600f), new VisemeMapping("m_EyelidsLookat_R", -0.000001f), new VisemeMapping("m_EyelidsLookat_U", 0.000000f), new VisemeMapping("m_EyelidsLookat_D", 0.000000f), new VisemeMapping("m_BlinksLookat_UD", 0.052699f), new VisemeMapping("m_BlinksLookat_LR", 0.053199f) } },
+ { "eye_top_Left", new[] { new VisemeMapping("eye_Left_RX+", 0.000000f), new VisemeMapping("eye_Left_RX-", 0.000000f), new VisemeMapping("eye_Left_RY+", 0.000000f), new VisemeMapping("eye_Left_RY-", 0.000000f), new VisemeMapping("eye_Left_RZ+", 0.000000f), new VisemeMapping("eye_Left_RZ-", 0.000000f) } },
+ { "eye_top_Right", new[] { new VisemeMapping("eye_Right_RX+", 0.000000f), new VisemeMapping("eye_Right_RX-", 0.000000f), new VisemeMapping("eye_Right_RY+", 0.000000f), new VisemeMapping("eye_Right_RY-", 0.000000f), new VisemeMapping("eye_Right_RZ+", 0.000000f), new VisemeMapping("eye_Right_RZ-", 0.000000f) } },
+ };
+
+ ///
+ /// All viseme animation names used in lip sync for Prothean
+ /// Note: Prothean uses unique morph target names with m_ prefix
+ ///
+ public static readonly string[] ProtheanVisemes =
+ [
+ // Head and neck rotations
+ "neck_RX+",
+ "neck_RX-",
+ "neck_RZ+",
+ "neck_RZ-",
+ "neck_RY+",
+ "neck_RY-",
+ "head_RX+",
+ "head_RX-",
+ "head_RY+",
+ "head_RY-",
+ "head_RZ+",
+ "head_RZ-",
+ // Brow controls
+ "m_CockedBrows_D",
+ "m_CockedBrows_R",
+ "m_CockedBrows_L",
+ "m_CockedBrows_U",
+ "m_EmotionBrows_D",
+ "m_EmotionBrows_R",
+ "m_EmotionBrows_L",
+ "m_EmotionBrows_U",
+ "m_UpDownBrow_LR",
+ "m_UpDownBrow_UD",
+ // Eyelid controls
+ "m_Squint_Eyelids_LR",
+ "m_Squint_Eyelids_UD",
+ "m_WideOpen_Eyelids_LR",
+ "m_WideOpen_Eyelids_UD",
+ "m_EyelidsLookat_L",
+ "m_EyelidsLookat_R",
+ "m_EyelidsLookat_U",
+ "m_EyelidsLookat_D",
+ "m_BlinksLookat_UD",
+ "m_BlinksLookat_LR",
+ // Jaw controls
+ "m_Open_LR",
+ "m_Open_UD",
+ "m_Open",
+ "m_JawRotate_U",
+ "m_JawRotate_D",
+ "m_JawRotate_L",
+ "m_JawRotate_R",
+ "m_Jaw-",
+ "m_Jaw+",
+ // Mouth shapes (phonemes)
+ "m_OH",
+ "m_EE",
+ "m_EH",
+ "m_OW",
+ "m_ZZ",
+ "m_TH",
+ "m_N",
+ "m_L",
+ "m_G",
+ "m_M",
+ "m_FV",
+ "m_Flap",
+ // Closed/Offset controls
+ "m_Closed_R",
+ "m_Closed_L",
+ "m_Closed_U",
+ "m_Closed_D",
+ "m_Offset_L",
+ "m_Offset_R",
+ "m_Offset_U",
+ "m_Offset_D",
+ // Expression controls
+ "m_Angry_UD",
+ "m_Angry_L",
+ "m_Angry_R",
+ "m_Smile_Frown_U",
+ "m_Smile_Frown_D",
+ "m_Smile_Frown_L",
+ "m_Smile_Frown_R",
+ "m_Sneer_UD",
+ "m_Sneer_LR",
+ // Eye rotations
+ "eye_Right_RX+",
+ "eye_Right_RX-",
+ "eye_Right_RY+",
+ "eye_Right_RY-",
+ "eye_Right_RZ+",
+ "eye_Right_RZ-",
+ "eye_Left_RX+",
+ "eye_Left_RX-",
+ "eye_Left_RY+",
+ "eye_Left_RY-",
+ "eye_Left_RZ+",
+ "eye_Left_RZ-"
+ ];
+
+ ///
+ /// Yahg phoneme to viseme mappings - from SFX_Yahg_FaceFX data.
+ /// Note: Yahg has a unique multi-jawed face with fins and 4 pairs of eyes.
+ ///
+ public static readonly Dictionary YahgPhonemeMap = new()
+ {
+ // Yahg phoneme mappings from SFX_Yahg_FaceFX
+ { "Neck", new[] { new VisemeMapping("neck_RX+", 0.000000f), new VisemeMapping("neck_RX-", 0.000000f), new VisemeMapping("neck_RZ+", 0.000000f), new VisemeMapping("neck_RZ-", 0.000000f), new VisemeMapping("neck_RY+", 0.000000f), new VisemeMapping("neck_RY-", 0.000000f) } },
+ { "Head", new[] { new VisemeMapping("head_RX+", 0.000000f), new VisemeMapping("head_RX-", 0.000000f), new VisemeMapping("head_RY+", 0.000000f), new VisemeMapping("head_RY-", 0.000000f), new VisemeMapping("head_RZ+", 0.000000f), new VisemeMapping("head_RZ-", 0.000000f) } },
+ { "HeadBase", new[] { new VisemeMapping("m_FaceOpen", 10.484099f) } },
+ { "Jaw", new[] { new VisemeMapping("m_JawOpen", 0.647744f), new VisemeMapping("m_JawClench", 0.019733f), new VisemeMapping("m_FaceOpen", -1.036158f), new VisemeMapping("m_FaceClose", 0.000000f) } },
+ { "LeftLipBtm3", new[] { new VisemeMapping("m_BLipCurlLMidOut", 0.000000f), new VisemeMapping("m_BLipCurlLMidIn", 0.000000f), new VisemeMapping("m_BLipCurlLOut", 0.000000f), new VisemeMapping("m_BLipCurlLIn", 0.000000f), new VisemeMapping("m_FaceOpen", -1.118356f) } },
+ { "LeftChin", new[] { new VisemeMapping("m_LCFinUp", 0.000000f), new VisemeMapping("m_LCFinDown", 0.000000f), new VisemeMapping("m_LCFinSplayOut", 0.000000f), new VisemeMapping("m_LCFinSplayIn", 0.000000f) } },
+ { "LeftFin1", new[] { new VisemeMapping("m_LFinFlare", 3.119480f), new VisemeMapping("m_LFinFold", 0.995887f), new VisemeMapping("m_LFinRotUp", 0.000000f), new VisemeMapping("m_LFinRotDown", 0.000000f) } },
+ { "RightFin1", new[] { new VisemeMapping("m_RFinFlare", 3.092630f), new VisemeMapping("m_RFinFold", 0.969497f), new VisemeMapping("m_RFinRotUp", 0.000000f), new VisemeMapping("m_RFinRotDown", 0.000000f) } },
+ { "RightLipBtm3", new[] { new VisemeMapping("m_BLipCurlRMidOut", 0.000000f), new VisemeMapping("m_BLipCurlRMidIn", 0.000000f), new VisemeMapping("m_BLipCurlROut", 0.000000f), new VisemeMapping("m_BLipCurlRIn", 0.000000f), new VisemeMapping("m_FaceOpen", -1.118360f) } },
+ { "RightChin", new[] { new VisemeMapping("m_RCFinUp", 0.000000f), new VisemeMapping("m_RCFinDown", 0.000000f), new VisemeMapping("m_RCFinSplayOut", 0.000000f), new VisemeMapping("m_RCFinSplayIn", 0.000000f) } },
+ { "LipBtmBase", new[] { new VisemeMapping("m_BBLipCurlOut", 0.000000f), new VisemeMapping("m_BBLipCurlIn", 0.000000f) } },
+ { "LeftLipBtm1", new[] { new VisemeMapping("m_BLipCurlLMidOut", 0.000000f), new VisemeMapping("m_BLipCurlLMidIn", 0.000000f), new VisemeMapping("m_BLipCurlLOut", 0.000000f), new VisemeMapping("m_BLipCurlLIn", 0.000000f) } },
+ { "RightLipBtm1", new[] { new VisemeMapping("m_BLipCurlRMidOut", 0.000000f), new VisemeMapping("m_BLipCurlRMidIn", 0.000000f), new VisemeMapping("m_BLipCurlROut", 0.000000f), new VisemeMapping("m_BLipCurlRIn", 0.000000f) } },
+ { "RightLipBtm2", new[] { new VisemeMapping("m_BLipCurlRMidOut", 0.000000f), new VisemeMapping("m_BLipCurlRMidIn", 0.000000f), new VisemeMapping("m_BLipCurlROut", 0.000000f), new VisemeMapping("m_BLipCurlRIn", 0.000000f), new VisemeMapping("m_FaceOpen", -0.000003f) } },
+ { "LeftLipBtm2", new[] { new VisemeMapping("m_BLipCurlLMidOut", 0.000000f), new VisemeMapping("m_BLipCurlLMidIn", 0.000000f), new VisemeMapping("m_BLipCurlLOut", 0.000000f), new VisemeMapping("m_BLipCurlLIn", 0.000000f), new VisemeMapping("m_FaceOpen", -0.000003f) } },
+ { "MouthBottom", new[] { new VisemeMapping("m_InMouthContract", 1.372421f), new VisemeMapping("m_InMouthOut", 8.203371f), new VisemeMapping("m_InMouthRelax", -2.329309f), new VisemeMapping("m_InMouthIn", -14.095701f) } },
+ { "LeftFin2", new[] { new VisemeMapping("m_LFinFlare", 3.005980f), new VisemeMapping("m_LFinFold", 0.764654f), new VisemeMapping("m_LFinRotUp", 0.000000f), new VisemeMapping("m_LFinRotDown", 0.000000f) } },
+ { "RightFin2", new[] { new VisemeMapping("m_RFinFlare", 2.984540f), new VisemeMapping("m_RFinFold", 0.744391f), new VisemeMapping("m_RFinRotUp", 0.000000f), new VisemeMapping("m_RFinRotDown", 0.000000f) } },
+ { "LeftFaceBase", new[] { new VisemeMapping("m_FaceOpen", -0.000001f), new VisemeMapping("m_FaceClose", 0.000000f) } },
+ { "LeftLipTop1", new[] { new VisemeMapping("m_LLipCurlTopOut", 0.000000f), new VisemeMapping("m_LLipCurlTopIn", 0.000000f), new VisemeMapping("m_LLipCurlMidOut", 0.000000f), new VisemeMapping("m_LLipCurlMidIn", 0.000000f), new VisemeMapping("m_FaceOpen", 0.306275f) } },
+ { "LeftLipTop2", new[] { new VisemeMapping("m_LLipCurlTopOut", 0.000000f), new VisemeMapping("m_LLipCurlTopIn", 0.000000f), new VisemeMapping("m_LLipCurlMidOut", 0.000000f), new VisemeMapping("m_LLipCurlMidIn", 0.000000f), new VisemeMapping("m_LLipCurlLowOut", 0.000000f), new VisemeMapping("m_LLipCurlLowIn", 0.000000f), new VisemeMapping("m_FaceOpen", 0.074781f) } },
+ { "LeftLipTop3", new[] { new VisemeMapping("m_LLipCurlTopOut", 0.000000f), new VisemeMapping("m_LLipCurlTopIn", 0.000000f), new VisemeMapping("m_LLipCurlMidOut", 0.000000f), new VisemeMapping("m_LLipCurlMidIn", 0.000000f), new VisemeMapping("m_LLipCurlLowOut", 0.000000f), new VisemeMapping("m_LLipCurlLowIn", 0.000000f), new VisemeMapping("m_FaceOpen", 0.368922f) } },
+ { "LeftLipTop4", new[] { new VisemeMapping("m_LLipCurlLowOut", 0.000000f), new VisemeMapping("m_LLipCurlLowIn", 0.000000f), new VisemeMapping("m_FaceOpen", 0.803947f) } },
+ { "LeftEyeLid3Btm", new[] { new VisemeMapping("m_EyeLowInWide", 0.120258f), new VisemeMapping("m_EyeLowInBlink", -0.158587f), new VisemeMapping("m_L3EyeLidLowUp", -0.182460f), new VisemeMapping("m_L3EyeLidLowDown", 0.366809f) } },
+ { "LeftEyeLid3Top", new[] { new VisemeMapping("m_EyeLowInWide", 0.129472f), new VisemeMapping("m_EyeLowInBlink", -0.175843f), new VisemeMapping("m_L3EyeLidTopUp", 0.073894f), new VisemeMapping("m_L3EyeLidTopDown", -0.386891f) } },
+ { "LeftBrow3", new[] { new VisemeMapping("m_L3BrowUp", -0.221134f), new VisemeMapping("m_L3BrowDown", 0.277736f), new VisemeMapping("m_L3BrowOut", 1.071668f), new VisemeMapping("m_L3BrowIn", -0.659603f), new VisemeMapping("m_L3BrowRotOut", 0.000000f), new VisemeMapping("m_L3BrowRotIn", 0.000000f) } },
+ { "LeftEyeLid1Btm", new[] { new VisemeMapping("m_EyeTopInWide", -0.159580f), new VisemeMapping("m_EyeTopInBlink", -0.450143f), new VisemeMapping("m_L1EyeLidLowUp", -0.172322f), new VisemeMapping("m_L1EyeLidLowDown", 0.272523f) } },
+ { "LeftEyeLid1Top", new[] { new VisemeMapping("m_EyeTopInWide", 0.073172f), new VisemeMapping("m_EyeTopInBlink", -1.202842f), new VisemeMapping("m_L1EyeLidTopUp", 0.407610f), new VisemeMapping("m_L1EyeLidTopDown", -1.134641f) } },
+ { "LeftBrow1", new[] { new VisemeMapping("m_L1BrowUp", -0.320147f), new VisemeMapping("m_L1BrowDown", 0.445965f), new VisemeMapping("m_L1BrowOut", 0.742152f), new VisemeMapping("m_L1BrowIn", -0.613498f), new VisemeMapping("m_L1BrowRotOut", 0.000000f), new VisemeMapping("m_L1BrowRotIn", 0.000000f) } },
+ { "LeftCheek1", new[] { new VisemeMapping("m_LCheekOut", -0.284754f), new VisemeMapping("m_LCheekIn", 0.240236f), new VisemeMapping("m_LCheekUp", -0.489461f), new VisemeMapping("m_LCheekDown", 0.603806f) } },
+ { "LeftCheek2", new[] { new VisemeMapping("m_LCheekOut", 2.299129f), new VisemeMapping("m_LCheekIn", -1.939702f), new VisemeMapping("m_LCheekUp", -2.241601f), new VisemeMapping("m_LCheekDown", 2.765291f) } },
+ { "LeftBrow4", new[] { new VisemeMapping("m_L4BrowUp", -0.424752f), new VisemeMapping("m_L4BrowDown", 0.638373f), new VisemeMapping("m_L4BrowOut", 2.492081f), new VisemeMapping("m_L4BrowIn", -2.009891f), new VisemeMapping("m_L4BrowRotOut", 0.000000f), new VisemeMapping("m_L4BrowRotIn", 0.000000f) } },
+ { "LeftBrow2", new[] { new VisemeMapping("m_L2BrowUp", -0.837227f), new VisemeMapping("m_L2BrowDown", 0.827205f), new VisemeMapping("m_L2BrowOut", 0.687475f), new VisemeMapping("m_L2BrowIn", -1.669969f), new VisemeMapping("m_L2BrowRotOut", 0.000000f), new VisemeMapping("m_L2BrowRotIn", 0.000000f) } },
+ { "LeftEyeLid2Top", new[] { new VisemeMapping("m_EyeTopOutWide", -0.094891f), new VisemeMapping("m_EyeTopOutBlink", -0.116404f), new VisemeMapping("m_L2EyeLidTopUp", 0.016263f), new VisemeMapping("m_L2EyeLidTopDown", -0.022547f) } },
+ { "LeftEyeLid2Btm", new[] { new VisemeMapping("m_EyeTopOutWide", 0.236201f), new VisemeMapping("m_EyeTopOutBlink", -0.544777f), new VisemeMapping("m_L2EyeLidLowUp", -0.771935f), new VisemeMapping("m_L2EyeLidLowDown", 0.440722f) } },
+ { "LeftEyeLid4Top", new[] { new VisemeMapping("m_EyeLowOutWide", 0.092134f), new VisemeMapping("m_EyeLowOutBlink", -0.616920f), new VisemeMapping("m_L4EyeLidTopUp", 0.127346f), new VisemeMapping("m_L4EyeLidTopDown", -0.520843f) } },
+ { "LeftEyeLid4Btm", new[] { new VisemeMapping("m_EyeLowOutWide", 0.107629f), new VisemeMapping("m_EyeLowOutBlink", -0.301813f), new VisemeMapping("m_L4EyeLidLowUp", -0.336879f), new VisemeMapping("m_L4EyeLidLowDown", 0.210599f) } },
+ { "MouthTopLeft", new[] { new VisemeMapping("m_InMouthContract", -2.148889f), new VisemeMapping("m_InMouthOut", -9.308189f), new VisemeMapping("m_InMouthRelax", 3.555179f), new VisemeMapping("m_InMouthIn", 13.744499f) } },
+ { "RightFaceBase", new[] { new VisemeMapping("m_FaceOpen", -0.000001f), new VisemeMapping("m_FaceClose", 0.000000f) } },
+ { "RightLipTop1", new[] { new VisemeMapping("m_RLipCurlTopOut", 0.000000f), new VisemeMapping("m_RLipCurlTopIn", 0.000000f), new VisemeMapping("m_RLipCurlMidOut", 0.000000f), new VisemeMapping("m_RLipCurlMidIn", 0.000000f), new VisemeMapping("m_FaceOpen", -0.315406f) } },
+ { "RightLipTop2", new[] { new VisemeMapping("m_RLipCurlTopOut", 0.000000f), new VisemeMapping("m_RLipCurlTopIn", 0.000000f), new VisemeMapping("m_RLipCurlMidOut", 0.000000f), new VisemeMapping("m_RLipCurlMidIn", 0.000000f), new VisemeMapping("m_RLipCurlLowOut", 0.000000f), new VisemeMapping("m_RLipCurlLowIn", 0.000000f), new VisemeMapping("m_FaceOpen", -0.051830f) } },
+ { "RightLipTop3", new[] { new VisemeMapping("m_RLipCurlTopOut", 0.000000f), new VisemeMapping("m_RLipCurlTopIn", 0.000000f), new VisemeMapping("m_RLipCurlMidOut", 0.000000f), new VisemeMapping("m_RLipCurlMidIn", 0.000000f), new VisemeMapping("m_RLipCurlLowOut", 0.000000f), new VisemeMapping("m_RLipCurlLowIn", 0.000000f), new VisemeMapping("m_FaceOpen", -0.288717f) } },
+ { "RightLipTop4", new[] { new VisemeMapping("m_RLipCurlLowOut", 0.000000f), new VisemeMapping("m_RLipCurlLowIn", 0.000000f), new VisemeMapping("m_FaceOpen", 0.386889f) } },
+ { "RightEyeLid3Btm", new[] { new VisemeMapping("m_EyeLowInWide", -0.120262f), new VisemeMapping("m_EyeLowInBlink", 0.158592f), new VisemeMapping("m_R3EyeLidLowUp", 0.187330f), new VisemeMapping("m_R3EyeLidLowDown", -0.250314f) } },
+ { "RightEyeLid3Top", new[] { new VisemeMapping("m_EyeLowInWide", -0.129473f), new VisemeMapping("m_EyeLowInBlink", 0.175840f), new VisemeMapping("m_R3EyeLidTopUp", -0.055202f), new VisemeMapping("m_R3EyeLidTopDown", 0.377562f) } },
+ { "RightBrow3", new[] { new VisemeMapping("m_R3BrowUp", 0.175496f), new VisemeMapping("m_R3BrowDown", -0.280254f), new VisemeMapping("m_R3BrowOut", -0.986670f), new VisemeMapping("m_R3BrowIn", 0.585998f), new VisemeMapping("m_R3BrowRotOut", 0.000000f), new VisemeMapping("m_R3BrowRotIn", 0.000000f) } },
+ { "RightEyeLid1Btm", new[] { new VisemeMapping("m_EyeTopInWide", 0.159581f), new VisemeMapping("m_EyeTopInBlink", 0.450141f), new VisemeMapping("m_R1EyeLidLowUp", 0.210226f), new VisemeMapping("m_R1EyeLidLowDown", -0.275908f) } },
+ { "RightEyeLid1Top", new[] { new VisemeMapping("m_EyeTopInWide", -0.073183f), new VisemeMapping("m_EyeTopInBlink", 1.202850f), new VisemeMapping("m_R1EyeLidTopUp", -0.328074f), new VisemeMapping("m_R1EyeLidTopDown", 1.102590f) } },
+ { "RightBrow1", new[] { new VisemeMapping("m_R1BrowUp", 0.315742f), new VisemeMapping("m_R1BrowDown", -0.438191f), new VisemeMapping("m_R1BrowOut", -0.813430f), new VisemeMapping("m_R1BrowIn", 0.591295f), new VisemeMapping("m_R1BrowRotOut", 0.000000f), new VisemeMapping("m_R1BrowRotIn", 0.000000f) } },
+ { "RightBrow2", new[] { new VisemeMapping("m_R2BrowUp", 0.654576f), new VisemeMapping("m_R2BrowDown", -0.847932f), new VisemeMapping("m_R2BrowOut", -0.691819f), new VisemeMapping("m_R2BrowIn", 0.968189f), new VisemeMapping("m_R2BrowRotOut", 0.093332f), new VisemeMapping("m_R2BrowRotIn", 0.000000f) } },
+ { "RightEyeLid2Top", new[] { new VisemeMapping("m_EyeTopOutWide", 0.094891f), new VisemeMapping("m_EyeTopOutBlink", 0.116403f), new VisemeMapping("m_R2EyeLidTopUp", -0.058838f), new VisemeMapping("m_R2EyeLidTopDown", 0.236545f) } },
+ { "RightEyeLid2Btm", new[] { new VisemeMapping("m_EyeTopOutWide", -0.236202f), new VisemeMapping("m_EyeTopOutBlink", 0.544781f), new VisemeMapping("m_R2EyeLidLowUp", 0.754195f), new VisemeMapping("m_R2EyeLidLowDown", -0.449958f) } },
+ { "RightBrow4", new[] { new VisemeMapping("m_R4BrowUp", 0.459908f), new VisemeMapping("m_R4BrowDown", -0.529171f), new VisemeMapping("m_R4BrowOut", -2.319571f), new VisemeMapping("m_R4BrowIn", 2.148589f), new VisemeMapping("m_R4BrowRotOut", 0.000000f), new VisemeMapping("m_R4BrowRotIn", 0.000000f) } },
+ { "RightEyeLid4Top", new[] { new VisemeMapping("m_EyeLowOutWide", -0.092134f), new VisemeMapping("m_EyeLowOutBlink", 0.616919f), new VisemeMapping("m_R4EyeLidTopUp", -0.138492f), new VisemeMapping("m_R4EyeLidTopDown", 0.574798f) } },
+ { "RightEyeLid4Btm", new[] { new VisemeMapping("m_EyeLowOutWide", -0.107634f), new VisemeMapping("m_EyeLowOutBlink", 0.301818f), new VisemeMapping("m_R4EyeLidLowUp", 0.239602f), new VisemeMapping("m_R4EyeLidLowDown", -0.135381f) } },
+ { "RightCheek1", new[] { new VisemeMapping("m_RCheekOut", 0.296659f), new VisemeMapping("m_RCheekIn", -0.240531f), new VisemeMapping("m_RCheekUp", 0.466537f), new VisemeMapping("m_RCheekDown", -0.596921f) } },
+ { "RightCheek2", new[] { new VisemeMapping("m_RCheekOut", -2.394999f), new VisemeMapping("m_RCheekIn", 1.941870f), new VisemeMapping("m_RCheekUp", 2.136611f), new VisemeMapping("m_RCheekDown", -2.733738f) } },
+ { "MouthTopRight", new[] { new VisemeMapping("m_InMouthContract", 2.148889f), new VisemeMapping("m_InMouthOut", 9.308182f), new VisemeMapping("m_InMouthRelax", -3.555180f), new VisemeMapping("m_InMouthIn", -13.744500f) } },
+ { "MouthCenter", new[] { new VisemeMapping("m_InMouthContract", 2.257080f), new VisemeMapping("m_InMouthOut", 4.794481f), new VisemeMapping("m_InMouthRelax", -8.838210f), new VisemeMapping("m_InMouthIn", -14.095700f) } },
+ { "NeckTwist", new[] { new VisemeMapping("head_RX+", 0.000000f), new VisemeMapping("head_RX-", 0.000000f), new VisemeMapping("head_RY+", 0.000000f), new VisemeMapping("head_RY-", 0.000000f) } },
+ };
+
+ ///
+ /// All viseme animation names used in lip sync for Yahg
+ /// Note: Yahg has a unique facial structure with fins, 4 pairs of eyes, and complex jaw/lip system
+ ///
+ public static readonly string[] YahgVisemes =
+ [
+ // Head and neck rotations
+ "neck_RX+",
+ "neck_RX-",
+ "neck_RZ+",
+ "neck_RZ-",
+ "neck_RY+",
+ "neck_RY-",
+ "head_RX+",
+ "head_RX-",
+ "head_RY+",
+ "head_RY-",
+ "head_RZ+",
+ "head_RZ-",
+ // Face open/close
+ "m_FaceOpen",
+ "m_FaceClose",
+ // Jaw controls
+ "m_JawOpen",
+ "m_JawClench",
+ // Inner mouth controls
+ "m_InMouthContract",
+ "m_InMouthOut",
+ "m_InMouthRelax",
+ "m_InMouthIn",
+ // Left lip bottom curls
+ "m_BLipCurlLMidOut",
+ "m_BLipCurlLMidIn",
+ "m_BLipCurlLOut",
+ "m_BLipCurlLIn",
+ // Right lip bottom curls
+ "m_BLipCurlRMidOut",
+ "m_BLipCurlRMidIn",
+ "m_BLipCurlROut",
+ "m_BLipCurlRIn",
+ // Bottom lip base
+ "m_BBLipCurlOut",
+ "m_BBLipCurlIn",
+ // Left lip top curls
+ "m_LLipCurlTopOut",
+ "m_LLipCurlTopIn",
+ "m_LLipCurlMidOut",
+ "m_LLipCurlMidIn",
+ "m_LLipCurlLowOut",
+ "m_LLipCurlLowIn",
+ // Right lip top curls
+ "m_RLipCurlTopOut",
+ "m_RLipCurlTopIn",
+ "m_RLipCurlMidOut",
+ "m_RLipCurlMidIn",
+ "m_RLipCurlLowOut",
+ "m_RLipCurlLowIn",
+ // Left fin controls
+ "m_LFinFlare",
+ "m_LFinFold",
+ "m_LFinRotUp",
+ "m_LFinRotDown",
+ // Right fin controls
+ "m_RFinFlare",
+ "m_RFinFold",
+ "m_RFinRotUp",
+ "m_RFinRotDown",
+ // Left chin fin controls
+ "m_LCFinUp",
+ "m_LCFinDown",
+ "m_LCFinSplayOut",
+ "m_LCFinSplayIn",
+ // Right chin fin controls
+ "m_RCFinUp",
+ "m_RCFinDown",
+ "m_RCFinSplayOut",
+ "m_RCFinSplayIn",
+ // Eye lid controls (4 pairs of eyes)
+ "m_EyeLowInWide",
+ "m_EyeLowInBlink",
+ "m_EyeTopInWide",
+ "m_EyeTopInBlink",
+ "m_EyeTopOutWide",
+ "m_EyeTopOutBlink",
+ "m_EyeLowOutWide",
+ "m_EyeLowOutBlink",
+ // Left eye 1 (top inner)
+ "m_L1EyeLidLowUp",
+ "m_L1EyeLidLowDown",
+ "m_L1EyeLidTopUp",
+ "m_L1EyeLidTopDown",
+ // Left eye 2 (top outer)
+ "m_L2EyeLidLowUp",
+ "m_L2EyeLidLowDown",
+ "m_L2EyeLidTopUp",
+ "m_L2EyeLidTopDown",
+ // Left eye 3 (bottom inner)
+ "m_L3EyeLidLowUp",
+ "m_L3EyeLidLowDown",
+ "m_L3EyeLidTopUp",
+ "m_L3EyeLidTopDown",
+ // Left eye 4 (bottom outer)
+ "m_L4EyeLidLowUp",
+ "m_L4EyeLidLowDown",
+ "m_L4EyeLidTopUp",
+ "m_L4EyeLidTopDown",
+ // Right eye 1 (top inner)
+ "m_R1EyeLidLowUp",
+ "m_R1EyeLidLowDown",
+ "m_R1EyeLidTopUp",
+ "m_R1EyeLidTopDown",
+ // Right eye 2 (top outer)
+ "m_R2EyeLidLowUp",
+ "m_R2EyeLidLowDown",
+ "m_R2EyeLidTopUp",
+ "m_R2EyeLidTopDown",
+ // Right eye 3 (bottom inner)
+ "m_R3EyeLidLowUp",
+ "m_R3EyeLidLowDown",
+ "m_R3EyeLidTopUp",
+ "m_R3EyeLidTopDown",
+ // Right eye 4 (bottom outer)
+ "m_R4EyeLidLowUp",
+ "m_R4EyeLidLowDown",
+ "m_R4EyeLidTopUp",
+ "m_R4EyeLidTopDown",
+ // Brow controls (4 pairs)
+ "m_L1BrowUp",
+ "m_L1BrowDown",
+ "m_L1BrowOut",
+ "m_L1BrowIn",
+ "m_L1BrowRotOut",
+ "m_L1BrowRotIn",
+ "m_L2BrowUp",
+ "m_L2BrowDown",
+ "m_L2BrowOut",
+ "m_L2BrowIn",
+ "m_L2BrowRotOut",
+ "m_L2BrowRotIn",
+ "m_L3BrowUp",
+ "m_L3BrowDown",
+ "m_L3BrowOut",
+ "m_L3BrowIn",
+ "m_L3BrowRotOut",
+ "m_L3BrowRotIn",
+ "m_L4BrowUp",
+ "m_L4BrowDown",
+ "m_L4BrowOut",
+ "m_L4BrowIn",
+ "m_L4BrowRotOut",
+ "m_L4BrowRotIn",
+ "m_R1BrowUp",
+ "m_R1BrowDown",
+ "m_R1BrowOut",
+ "m_R1BrowIn",
+ "m_R1BrowRotOut",
+ "m_R1BrowRotIn",
+ "m_R2BrowUp",
+ "m_R2BrowDown",
+ "m_R2BrowOut",
+ "m_R2BrowIn",
+ "m_R2BrowRotOut",
+ "m_R2BrowRotIn",
+ "m_R3BrowUp",
+ "m_R3BrowDown",
+ "m_R3BrowOut",
+ "m_R3BrowIn",
+ "m_R3BrowRotOut",
+ "m_R3BrowRotIn",
+ "m_R4BrowUp",
+ "m_R4BrowDown",
+ "m_R4BrowOut",
+ "m_R4BrowIn",
+ "m_R4BrowRotOut",
+ "m_R4BrowRotIn",
+ // Cheek controls
+ "m_LCheekOut",
+ "m_LCheekIn",
+ "m_LCheekUp",
+ "m_LCheekDown",
+ "m_RCheekOut",
+ "m_RCheekIn",
+ "m_RCheekUp",
+ "m_RCheekDown"
+ ];
+
+ ///
+ /// Human Child phoneme to viseme mappings - from SFX_HumanChild_FaceFX data.
+ /// Uses similar morph targets to adult humans but with child-specific values.
+ ///
+ public static readonly Dictionary HumanChildPhonemeMap = new()
+ {
+ // Human Child phoneme mappings from SFX_HumanChild_FaceFX
+ { "Neck", new[] { new VisemeMapping("neck_RX+", 0.000000f), new VisemeMapping("neck_RX-", 0.000000f), new VisemeMapping("neck_RZ+", 0.000000f), new VisemeMapping("neck_RZ-", 0.000000f), new VisemeMapping("neck_RY+", 0.000000f), new VisemeMapping("neck_RY-", 0.000000f) } },
+ { "Head", new[] { new VisemeMapping("head_RX+", 0.000000f), new VisemeMapping("head_RX-", 0.000000f), new VisemeMapping("head_RY+", 0.000000f), new VisemeMapping("head_RY-", 0.000000f), new VisemeMapping("head_RZ+", 0.000000f), new VisemeMapping("head_RZ-", 0.000000f) } },
+ { "brow_right", new[] { new VisemeMapping("m_CockedBrows_D", 0.360201f), new VisemeMapping("m_EmotionBrows_D", 0.000000f), new VisemeMapping("m_CockedBrows_R", 0.472400f), new VisemeMapping("m_CockedBrows_L", 0.260099f), new VisemeMapping("m_CockedBrows_U", 0.000000f), new VisemeMapping("m_EmotionBrows_R", 0.079000f), new VisemeMapping("m_UpDownBrow_LR", 0.151199f), new VisemeMapping("m_UpDownBrow_UD", 0.000000f), new VisemeMapping("m_EmotionBrows_L", 0.000199f), new VisemeMapping("m_EmotionBrows_U", 0.321300f) } },
+ { "eyeBlink_Right", new[] { new VisemeMapping("m_Squint_Eyelids_UD", 0.000000f), new VisemeMapping("m_WideOpen_Eyelids_LR", 0.000000f), new VisemeMapping("m_WideOpen_Eyelids_UD", 0.000000f), new VisemeMapping("m_EyelidsLookat_L", 0.002000f), new VisemeMapping("m_EyelidsLookat_R", 0.001800f), new VisemeMapping("m_EyelidsLookat_U", 0.002000f), new VisemeMapping("m_EyelidsLookat_D", 0.001900f), new VisemeMapping("m_BlinksLookat_UD", 0.000000f), new VisemeMapping("m_BlinksLookat_LR", 0.000000f) } },
+ { "jawBone", new[] { new VisemeMapping("m_JawRotate_L", 0.124603f), new VisemeMapping("m_Jaw+", 0.000000f), new VisemeMapping("m_Jaw-", 0.000000f), new VisemeMapping("m_Open_UD", 0.000000f), new VisemeMapping("m_JawRotate_U", 0.000008f), new VisemeMapping("m_JawRotate_D", 0.000008f), new VisemeMapping("m_Open_LR", 0.000000f), new VisemeMapping("m_JawRotate_R", 0.124596f), new VisemeMapping("m_Angry_UD", 0.000008f), new VisemeMapping("m_Smile_Frown_U", 0.000000f), new VisemeMapping("m_Smile_Frown_D", 0.116302f) } },
+ { "Tongue", new[] { new VisemeMapping("m_TH", -0.528008f), new VisemeMapping("m_L", -1.654900f), new VisemeMapping("m_Flap", -1.623901f), new VisemeMapping("m_Closed_R", 0.209900f), new VisemeMapping("m_Offset_L", 0.001099f), new VisemeMapping("m_Closed_U", 0.208496f), new VisemeMapping("m_Offset_R", 0.001099f), new VisemeMapping("m_Offset_U", 0.001099f), new VisemeMapping("m_Open_UD", -0.577415f), new VisemeMapping("m_Open_LR", 0.207397f), new VisemeMapping("m_Offset_D", 0.001099f), new VisemeMapping("m_Angry_UD", 0.206993f), new VisemeMapping("m_Smile_Frown_U", 0.181297f), new VisemeMapping("m_Smile_Frown_D", 0.206894f), new VisemeMapping("m_Smile_Frown_L", 0.206100f), new VisemeMapping("m_Smile_Frown_R", 0.206100f), new VisemeMapping("m_Closed_L", 0.206596f), new VisemeMapping("m_Angry_L", 0.149498f), new VisemeMapping("m_Angry_R", 0.149498f) } },
+ { "lowerCheek_right", new[] { new VisemeMapping("m_JawRotate_L", 0.000000f), new VisemeMapping("m_Closed_R", 0.473000f), new VisemeMapping("m_Offset_L", -0.000008f), new VisemeMapping("m_Closed_U", 0.178703f), new VisemeMapping("m_Offset_R", 0.000000f), new VisemeMapping("m_Offset_U", -0.467094f), new VisemeMapping("m_Open_UD", -0.367516f), new VisemeMapping("m_Open_LR", -0.000008f), new VisemeMapping("m_Closed_D", 0.000000f), new VisemeMapping("m_Offset_D", 0.522705f), new VisemeMapping("m_Angry_UD", 0.528992f), new VisemeMapping("m_Smile_Frown_U", -0.042305f), new VisemeMapping("m_Smile_Frown_D", 0.748794f), new VisemeMapping("m_Smile_Frown_L", -0.000298f), new VisemeMapping("m_Smile_Frown_R", -0.924004f), new VisemeMapping("m_Closed_L", -0.000008f), new VisemeMapping("m_Angry_R", 0.000000f) } },
+ { "LowerCheek_left", new[] { new VisemeMapping("m_JawRotate_L", -0.000404f), new VisemeMapping("m_Closed_R", 0.472900f), new VisemeMapping("m_Offset_L", -0.000008f), new VisemeMapping("m_Closed_U", 0.178703f), new VisemeMapping("m_Offset_R", 0.000000f), new VisemeMapping("m_Offset_U", -0.467094f), new VisemeMapping("m_Open_UD", -0.364616f), new VisemeMapping("m_Open_LR", -0.000015f), new VisemeMapping("m_Closed_D", 0.000000f), new VisemeMapping("m_Offset_D", 0.522705f), new VisemeMapping("m_Angry_UD", 0.528992f), new VisemeMapping("m_Smile_Frown_U", -0.042305f), new VisemeMapping("m_Smile_Frown_D", 0.748787f), new VisemeMapping("m_Smile_Frown_L", -0.923706f), new VisemeMapping("m_Smile_Frown_R", -0.000008f), new VisemeMapping("m_Closed_L", -0.000008f), new VisemeMapping("m_Angry_L", 0.000000f), new VisemeMapping("m_Angry_R", 0.000000f) } },
+ { "innerLowLip_left", new[] { new VisemeMapping("m_Closed_R", -0.131004f), new VisemeMapping("m_Offset_L", 0.000000f), new VisemeMapping("m_Closed_U", -0.409996f), new VisemeMapping("m_Offset_R", 0.000008f), new VisemeMapping("m_Offset_U", -0.467102f), new VisemeMapping("m_Open_UD", 0.704094f), new VisemeMapping("m_Closed_D", 0.821503f), new VisemeMapping("m_Offset_D", 0.522698f), new VisemeMapping("m_Angry_UD", 0.000000f), new VisemeMapping("m_Smile_Frown_U", 0.024696f), new VisemeMapping("m_Smile_Frown_D", 0.000008f), new VisemeMapping("m_Smile_Frown_L", -0.000298f), new VisemeMapping("m_Smile_Frown_R", -0.145004f), new VisemeMapping("m_Closed_L", 0.000008f), new VisemeMapping("m_Angry_L", -0.180008f), new VisemeMapping("m_Angry_R", -0.180008f) } },
+ { "lowerLip_left", new[] { new VisemeMapping("m_OH", -0.089996f), new VisemeMapping("m_EE", 0.007095f), new VisemeMapping("m_EH", 0.115997f), new VisemeMapping("m_OW", -0.002998f), new VisemeMapping("m_ZZ", 0.000000f), new VisemeMapping("m_TH", -0.226006f), new VisemeMapping("m_N", 0.000000f), new VisemeMapping("m_L", -0.040001f), new VisemeMapping("m_G", -0.002007f), new VisemeMapping("m_Open", -0.191002f), new VisemeMapping("m_Flap", 0.000000f), new VisemeMapping("m_Closed_R", 0.054909f), new VisemeMapping("m_Closed_U", -0.287197f), new VisemeMapping("m_Open_UD", -0.285099f), new VisemeMapping("m_Open_LR", 0.000000f), new VisemeMapping("m_Closed_D", -0.533188f), new VisemeMapping("m_M", -0.101295f), new VisemeMapping("m_FV", -0.349998f), new VisemeMapping("m_Angry_UD", -0.077797f), new VisemeMapping("m_Smile_Frown_U", -0.420601f), new VisemeMapping("m_Smile_Frown_D", -0.111206f), new VisemeMapping("m_Smile_Frown_L", 0.115906f), new VisemeMapping("m_Smile_Frown_R", -0.204994f), new VisemeMapping("m_Closed_L", -0.070999f), new VisemeMapping("m_Angry_L", 0.250900f), new VisemeMapping("m_Angry_R", 0.136902f) } },
+ { "innerLowLip_right", new[] { new VisemeMapping("m_Closed_R", -0.131004f), new VisemeMapping("m_Offset_L", -0.000000f), new VisemeMapping("m_Closed_U", -0.409996f), new VisemeMapping("m_Offset_R", 0.000000f), new VisemeMapping("m_Offset_U", -0.467102f), new VisemeMapping("m_Open_UD", 0.704185f), new VisemeMapping("m_Closed_D", 0.821503f), new VisemeMapping("m_Offset_D", 0.522697f), new VisemeMapping("m_Angry_UD", 0.000000f), new VisemeMapping("m_Smile_Frown_U", 0.024696f), new VisemeMapping("m_Smile_Frown_D", -0.000008f), new VisemeMapping("m_Smile_Frown_L", -0.144707f), new VisemeMapping("m_Smile_Frown_R", 0.000000f), new VisemeMapping("m_Closed_L", 0.000008f), new VisemeMapping("m_Angry_L", -0.180008f), new VisemeMapping("m_Angry_R", -0.180008f) } },
+ { "lowerLip_right", new[] { new VisemeMapping("m_OH", -0.090004f), new VisemeMapping("m_EE", 0.007103f), new VisemeMapping("m_EH", 0.115997f), new VisemeMapping("m_OW", -0.003006f), new VisemeMapping("m_ZZ", 0.000000f), new VisemeMapping("m_TH", -0.225998f), new VisemeMapping("m_N", 0.000000f), new VisemeMapping("m_L", -0.040001f), new VisemeMapping("m_G", -0.002007f), new VisemeMapping("m_Open", -0.191002f), new VisemeMapping("m_Flap", 0.000000f), new VisemeMapping("m_JawRotate_L", -0.072807f), new VisemeMapping("m_Closed_R", 0.049103f), new VisemeMapping("m_Closed_U", -0.287403f), new VisemeMapping("m_Open_UD", -0.290199f), new VisemeMapping("m_Open_LR", -0.000008f), new VisemeMapping("m_Closed_D", -0.593895f), new VisemeMapping("m_M", -0.101402f), new VisemeMapping("m_FV", -0.362999f), new VisemeMapping("m_Angry_UD", -0.078094f), new VisemeMapping("m_Smile_Frown_U", -0.415901f), new VisemeMapping("m_Smile_Frown_D", -0.111206f), new VisemeMapping("m_Smile_Frown_L", -0.288399f), new VisemeMapping("m_Smile_Frown_R", 0.292793f), new VisemeMapping("m_Closed_L", -0.070999f), new VisemeMapping("m_Angry_L", 0.136406f), new VisemeMapping("m_Angry_R", 0.187202f) } },
+ { "cheek_right", new[] { new VisemeMapping("m_OH", 0.494598f), new VisemeMapping("m_EE", 0.000000f), new VisemeMapping("m_EH", 0.219704f), new VisemeMapping("m_OW", -0.163300f), new VisemeMapping("m_ZZ", -0.318306f), new VisemeMapping("m_TH", -0.117302f), new VisemeMapping("m_N", -0.173302f), new VisemeMapping("m_L", -0.085297f), new VisemeMapping("m_G", -0.212303f), new VisemeMapping("m_Open", 0.357597f), new VisemeMapping("m_Flap", 0.205803f), new VisemeMapping("m_JawRotate_L", 1.145897f), new VisemeMapping("m_Closed_R", 0.000000f), new VisemeMapping("m_Offset_L", 0.000000f), new VisemeMapping("m_Closed_U", -0.000000f), new VisemeMapping("m_Offset_R", 0.000000f), new VisemeMapping("m_Offset_U", 0.000000f), new VisemeMapping("m_Open_UD", 1.915703f), new VisemeMapping("m_Open_LR", 0.158203f), new VisemeMapping("m_JawRotate_R", -0.831406f), new VisemeMapping("m_Closed_D", 0.000000f), new VisemeMapping("m_Offset_D", 0.000000f), new VisemeMapping("m_M", -0.337296f), new VisemeMapping("m_FV", -0.163300f), new VisemeMapping("m_Angry_UD", -0.737900f), new VisemeMapping("m_Sneer_UD", -0.407402f), new VisemeMapping("m_Smile_Frown_U", -0.521996f), new VisemeMapping("m_Smile_Frown_D", 0.652802f), new VisemeMapping("m_Smile_Frown_L", 0.000000f), new VisemeMapping("m_Smile_Frown_R", -1.308998f), new VisemeMapping("m_Closed_L", -0.000000f), new VisemeMapping("m_Angry_L", -0.000298f), new VisemeMapping("m_Angry_R", 0.044106f) } },
+ { "cheek_left", new[] { new VisemeMapping("m_OH", 0.494598f), new VisemeMapping("m_EE", -0.000008f), new VisemeMapping("m_EH", 0.219704f), new VisemeMapping("m_OW", -0.163300f), new VisemeMapping("m_ZZ", -0.318306f), new VisemeMapping("m_TH", -0.117302f), new VisemeMapping("m_N", -0.173302f), new VisemeMapping("m_L", -0.085297f), new VisemeMapping("m_G", -0.212303f), new VisemeMapping("m_Open", 0.357597f), new VisemeMapping("m_Flap", 0.205795f), new VisemeMapping("m_JawRotate_L", -0.831406f), new VisemeMapping("m_Closed_R", -0.005402f), new VisemeMapping("m_Offset_L", 0.000000f), new VisemeMapping("m_Closed_U", 0.000000f), new VisemeMapping("m_Offset_R", 0.000000f), new VisemeMapping("m_Offset_U", 0.000000f), new VisemeMapping("m_Open_UD", 1.915703f), new VisemeMapping("m_Open_LR", 0.158203f), new VisemeMapping("m_JawRotate_R", 1.146095f), new VisemeMapping("m_Closed_D", 0.000000f), new VisemeMapping("m_Sneer_LR", -0.407303f), new VisemeMapping("m_Offset_D", 0.000000f), new VisemeMapping("m_M", -0.337303f), new VisemeMapping("m_FV", -0.163300f), new VisemeMapping("m_Angry_UD", -0.737709f), new VisemeMapping("m_Smile_Frown_U", -0.522003f), new VisemeMapping("m_Smile_Frown_D", 0.539795f), new VisemeMapping("m_Smile_Frown_L", -1.309006f), new VisemeMapping("m_Smile_Frown_R", 0.000000f), new VisemeMapping("m_Closed_L", -0.000297f), new VisemeMapping("m_Angry_L", 0.044098f), new VisemeMapping("m_Angry_R", -0.000305f) } },
+ { "outerUpperLip_left", new[] { new VisemeMapping("m_OH", 0.074104f), new VisemeMapping("m_EE", 0.000000f), new VisemeMapping("m_EH", 0.000000f), new VisemeMapping("m_OW", 0.000000f), new VisemeMapping("m_ZZ", -0.087799f), new VisemeMapping("m_TH", 0.000000f), new VisemeMapping("m_N", 0.000000f), new VisemeMapping("m_G", 0.000000f), new VisemeMapping("m_Open", 0.000000f), new VisemeMapping("m_JawRotate_L", 0.351105f), new VisemeMapping("m_Closed_R", -0.012901f), new VisemeMapping("m_Offset_L", 0.000000f), new VisemeMapping("m_Closed_U", 0.000000f), new VisemeMapping("m_Offset_R", 0.000000f), new VisemeMapping("m_Offset_U", -0.466904f), new VisemeMapping("m_Open_UD", 0.360207f), new VisemeMapping("m_Open_LR", 0.000000f), new VisemeMapping("m_JawRotate_R", -0.509895f), new VisemeMapping("m_Closed_D", 0.000000f), new VisemeMapping("m_Offset_D", 0.522804f), new VisemeMapping("m_M", -0.248802f), new VisemeMapping("m_FV", -0.215797f), new VisemeMapping("m_Angry_UD", -0.323898f), new VisemeMapping("m_Smile_Frown_U", -0.039001f), new VisemeMapping("m_Smile_Frown_D", 0.021301f), new VisemeMapping("m_Smile_Frown_L", -0.685997f), new VisemeMapping("m_Smile_Frown_R", 0.000000f), new VisemeMapping("m_Closed_L", 0.000008f), new VisemeMapping("m_Angry_L", -0.179893f), new VisemeMapping("m_Angry_R", -0.272903f) } },
+ { "LipCorner_left", new[] { new VisemeMapping("m_OH", -0.013504f), new VisemeMapping("m_EH", 0.000000f), new VisemeMapping("m_ZZ", 0.000000f), new VisemeMapping("m_TH", 0.000000f), new VisemeMapping("m_N", 0.000000f), new VisemeMapping("m_L", 0.000000f), new VisemeMapping("m_Open", 0.163902f), new VisemeMapping("m_Closed_R", -0.212105f), new VisemeMapping("m_Offset_L", -0.001007f), new VisemeMapping("m_Closed_U", 0.157005f), new VisemeMapping("m_Open_UD", -0.017395f), new VisemeMapping("m_Open_LR", -0.000603f), new VisemeMapping("m_Closed_D", -0.021896f), new VisemeMapping("m_FV", 0.009598f), new VisemeMapping("m_Angry_UD", -0.051994f), new VisemeMapping("m_Smile_Frown_U", 0.014107f), new VisemeMapping("m_Smile_Frown_D", -0.000603f), new VisemeMapping("m_Smile_Frown_L", 0.157196f), new VisemeMapping("m_Smile_Frown_R", -0.033798f), new VisemeMapping("m_Closed_L", -0.052002f), new VisemeMapping("m_Angry_L", -0.109505f) } },
+ { "outerUpperLip_right", new[] { new VisemeMapping("m_OH", 0.074104f), new VisemeMapping("m_EE", 0.000000f), new VisemeMapping("m_EH", 0.000000f), new VisemeMapping("m_OW", 0.000000f), new VisemeMapping("m_ZZ", -0.087799f), new VisemeMapping("m_TH", 0.000000f), new VisemeMapping("m_N", 0.000000f), new VisemeMapping("m_L", 0.000000f), new VisemeMapping("m_G", 0.000000f), new VisemeMapping("m_Open", 0.000000f), new VisemeMapping("m_Flap", 0.156303f), new VisemeMapping("m_JawRotate_L", -0.509895f), new VisemeMapping("m_Closed_R", -0.001900f), new VisemeMapping("m_Offset_L", -0.000000f), new VisemeMapping("m_Closed_U", 0.000000f), new VisemeMapping("m_Offset_R", 0.000008f), new VisemeMapping("m_Offset_U", -0.466904f), new VisemeMapping("m_Open_UD", 0.360207f), new VisemeMapping("m_Open_LR", 0.000000f), new VisemeMapping("m_JawRotate_R", 0.302208f), new VisemeMapping("m_Closed_D", 0.000000f), new VisemeMapping("m_Offset_D", 0.522804f), new VisemeMapping("m_M", -0.248802f), new VisemeMapping("m_FV", -0.215797f), new VisemeMapping("m_Angry_UD", -0.323898f), new VisemeMapping("m_Smile_Frown_U", -0.039001f), new VisemeMapping("m_Smile_Frown_D", 0.021301f), new VisemeMapping("m_Smile_Frown_L", 0.000000f), new VisemeMapping("m_Smile_Frown_R", -0.418793f), new VisemeMapping("m_Closed_L", 0.000008f), new VisemeMapping("m_Angry_L", -0.272896f), new VisemeMapping("m_Angry_R", -0.179901f) } },
+ { "LipCorner_right", new[] { new VisemeMapping("m_OH", -0.013802f), new VisemeMapping("m_EH", -0.000694f), new VisemeMapping("m_ZZ", 0.000000f), new VisemeMapping("m_TH", -0.000008f), new VisemeMapping("m_N", 0.000000f), new VisemeMapping("m_L", 0.000000f), new VisemeMapping("m_Open", 0.163597f), new VisemeMapping("m_Closed_R", -0.210999f), new VisemeMapping("m_Offset_L", -0.001007f), new VisemeMapping("m_Closed_U", 0.156105f), new VisemeMapping("m_Open_UD", -0.017601f), new VisemeMapping("m_Open_LR", 0.000000f), new VisemeMapping("m_Closed_D", -0.021904f), new VisemeMapping("m_FV", 0.009300f), new VisemeMapping("m_Angry_UD", -0.051399f), new VisemeMapping("m_Smile_Frown_U", 0.015808f), new VisemeMapping("m_Smile_Frown_D", -0.000092f), new VisemeMapping("m_Smile_Frown_L", -0.034607f), new VisemeMapping("m_Smile_Frown_R", 0.156403f), new VisemeMapping("m_Closed_L", -0.052010f), new VisemeMapping("m_Angry_R", -0.109604f) } },
+ { "innerUpperLip_right", new[] { new VisemeMapping("m_JawRotate_L", -0.644997f), new VisemeMapping("m_Closed_R", -1.462006f), new VisemeMapping("m_Offset_L", -0.000000f), new VisemeMapping("m_Closed_U", 0.173500f), new VisemeMapping("m_Offset_R", 0.000000f), new VisemeMapping("m_Offset_U", -0.467102f), new VisemeMapping("m_Open_UD", 1.259995f), new VisemeMapping("m_JawRotate_R", 0.841400f), new VisemeMapping("m_Closed_D", 0.000000f), new VisemeMapping("m_Offset_D", 0.522697f), new VisemeMapping("m_Angry_UD", 0.000000f), new VisemeMapping("m_Sneer_UD", -2.915001f), new VisemeMapping("m_Smile_Frown_U", -0.073997f), new VisemeMapping("m_Smile_Frown_D", 0.116203f), new VisemeMapping("m_Smile_Frown_L", 0.388100f), new VisemeMapping("m_Smile_Frown_R", 0.791100f), new VisemeMapping("m_Closed_L", 0.000000f), new VisemeMapping("m_Angry_L", 1.728493f), new VisemeMapping("m_Angry_R", -0.180000f) } },
+ { "upperLip_right", new[] { new VisemeMapping("m_OH", 0.108101f), new VisemeMapping("m_EE", 0.000000f), new VisemeMapping("m_EH", 0.074898f), new VisemeMapping("m_OW", 0.082901f), new VisemeMapping("m_ZZ", 0.000000f), new VisemeMapping("m_TH", 0.025101f), new VisemeMapping("m_N", 0.003799f), new VisemeMapping("m_L", -0.000000f), new VisemeMapping("m_G", 0.067101f), new VisemeMapping("m_Open", 0.074799f), new VisemeMapping("m_Flap", 0.059097f), new VisemeMapping("m_JawRotate_L", 0.247002f), new VisemeMapping("m_Closed_R", 0.033096f), new VisemeMapping("m_Closed_U", -0.110199f), new VisemeMapping("m_Open_UD", -0.870102f), new VisemeMapping("m_Open_LR", 0.000000f), new VisemeMapping("m_JawRotate_R", -0.310890f), new VisemeMapping("m_Closed_D", 0.032501f), new VisemeMapping("m_M", -0.133003f), new VisemeMapping("m_FV", -0.018196f), new VisemeMapping("m_Angry_UD", 0.200104f), new VisemeMapping("m_Sneer_UD", 2.913300f), new VisemeMapping("m_Smile_Frown_U", -0.248901f), new VisemeMapping("m_Smile_Frown_D", -0.064400f), new VisemeMapping("m_Smile_Frown_L", -0.206406f), new VisemeMapping("m_Smile_Frown_R", -0.496185f), new VisemeMapping("m_Closed_L", 0.000000f), new VisemeMapping("m_Angry_L", -0.515709f), new VisemeMapping("m_Angry_R", 0.020309f) } },
+ { "innerUpperLip_left", new[] { new VisemeMapping("m_JawRotate_L", 0.841308f), new VisemeMapping("m_Closed_R", -1.438995f), new VisemeMapping("m_Offset_L", -0.000000f), new VisemeMapping("m_Closed_U", 0.173500f), new VisemeMapping("m_Offset_R", 0.000000f), new VisemeMapping("m_Offset_U", -0.467102f), new VisemeMapping("m_Open_UD", 1.625000f), new VisemeMapping("m_JawRotate_R", -0.644997f), new VisemeMapping("m_Closed_D", 0.000000f), new VisemeMapping("m_Sneer_LR", -2.930000f), new VisemeMapping("m_Offset_D", 0.522697f), new VisemeMapping("m_Angry_UD", 0.000000f), new VisemeMapping("m_Smile_Frown_U", -0.073997f), new VisemeMapping("m_Smile_Frown_D", 0.116203f), new VisemeMapping("m_Smile_Frown_L", 0.791100f), new VisemeMapping("m_Smile_Frown_R", 0.388100f), new VisemeMapping("m_Closed_L", 0.000000f), new VisemeMapping("m_Angry_L", -0.180000f), new VisemeMapping("m_Angry_R", 1.728493f) } },
+ { "upperLip_left", new[] { new VisemeMapping("m_OH", 0.108101f), new VisemeMapping("m_EE", -0.000008f), new VisemeMapping("m_EH", 0.074898f), new VisemeMapping("m_OW", 0.082893f), new VisemeMapping("m_ZZ", -0.000008f), new VisemeMapping("m_TH", 0.001099f), new VisemeMapping("m_N", 0.003799f), new VisemeMapping("m_L", 0.000000f), new VisemeMapping("m_G", 0.067101f), new VisemeMapping("m_Open", 0.074799f), new VisemeMapping("m_Flap", 0.059097f), new VisemeMapping("m_JawRotate_L", -0.303902f), new VisemeMapping("m_Closed_R", 0.028702f), new VisemeMapping("m_Closed_U", -0.102501f), new VisemeMapping("m_Open_UD", -0.961006f), new VisemeMapping("m_Open_LR", 0.000000f), new VisemeMapping("m_JawRotate_R", 0.311195f), new VisemeMapping("m_Closed_D", 0.032997f), new VisemeMapping("m_Sneer_LR", 2.928299f), new VisemeMapping("m_M", -0.132996f), new VisemeMapping("m_FV", -0.018295f), new VisemeMapping("m_Angry_UD", 0.200294f), new VisemeMapping("m_Smile_Frown_U", -0.249008f), new VisemeMapping("m_Smile_Frown_D", -0.064407f), new VisemeMapping("m_Smile_Frown_L", -0.625107f), new VisemeMapping("m_Smile_Frown_R", -0.023506f), new VisemeMapping("m_Closed_L", 0.000000f), new VisemeMapping("m_Angry_L", 0.019997f), new VisemeMapping("m_Angry_R", -0.476913f) } },
+ { "eye_Right", new[] { new VisemeMapping("eye_Right_RX+", 0.000000f), new VisemeMapping("eye_Right_RX-", 0.000000f), new VisemeMapping("eye_Right_RY+", 0.000000f), new VisemeMapping("eye_Right_RY-", 0.000000f), new VisemeMapping("eye_Right_RZ+", 0.000000f), new VisemeMapping("eye_Right_RZ-", 0.000000f) } },
+ { "eye_Left", new[] { new VisemeMapping("eye_Left_RX+", 0.000000f), new VisemeMapping("eye_Left_RX-", 0.000000f), new VisemeMapping("eye_Left_RY+", 0.000000f), new VisemeMapping("eye_Left_RY-", 0.000000f), new VisemeMapping("eye_Left_RZ+", 0.000000f), new VisemeMapping("eye_Left_RZ-", 0.000000f) } },
+ { "lowLid_Right", new[] { new VisemeMapping("m_Squint_Eyelids_UD", 0.000000f), new VisemeMapping("m_WideOpen_Eyelids_LR", 0.000000f), new VisemeMapping("m_WideOpen_Eyelids_UD", 0.000000f), new VisemeMapping("m_EyelidsLookat_L", 0.002000f), new VisemeMapping("m_EyelidsLookat_R", 0.001800f), new VisemeMapping("m_EyelidsLookat_U", 0.002000f), new VisemeMapping("m_EyelidsLookat_D", 0.001900f), new VisemeMapping("m_BlinksLookat_UD", 0.000000f), new VisemeMapping("m_BlinksLookat_LR", 0.000000f) } },
+ { "eyeBlink_Left", new[] { new VisemeMapping("m_Squint_Eyelids_LR", 0.000000f), new VisemeMapping("m_WideOpen_Eyelids_LR", 0.000000f), new VisemeMapping("m_WideOpen_Eyelids_UD", 0.000000f), new VisemeMapping("m_EyelidsLookat_L", 0.001600f), new VisemeMapping("m_EyelidsLookat_R", 0.001800f), new VisemeMapping("m_EyelidsLookat_U", 0.001800f), new VisemeMapping("m_EyelidsLookat_D", 0.001700f), new VisemeMapping("m_BlinksLookat_UD", 0.000000f), new VisemeMapping("m_BlinksLookat_LR", 0.000000f) } },
+ { "lowLid_Left", new[] { new VisemeMapping("m_Squint_Eyelids_LR", 0.000000f), new VisemeMapping("m_WideOpen_Eyelids_LR", 0.000000f), new VisemeMapping("m_WideOpen_Eyelids_UD", 0.000000f), new VisemeMapping("m_EyelidsLookat_L", 0.001600f), new VisemeMapping("m_EyelidsLookat_R", 0.001800f), new VisemeMapping("m_EyelidsLookat_U", 0.001800f), new VisemeMapping("m_EyelidsLookat_D", 0.001800f), new VisemeMapping("m_BlinksLookat_UD", 0.000000f), new VisemeMapping("m_BlinksLookat_LR", 0.000000f) } },
+ { "brow_left", new[] { new VisemeMapping("m_CockedBrows_D", -0.000099f), new VisemeMapping("m_EmotionBrows_D", 0.000000f), new VisemeMapping("m_CockedBrows_R", 0.259900f), new VisemeMapping("m_CockedBrows_L", 0.472300f), new VisemeMapping("m_CockedBrows_U", 0.360300f), new VisemeMapping("m_EmotionBrows_R", 0.213200f), new VisemeMapping("m_UpDownBrow_LR", 0.151300f), new VisemeMapping("m_UpDownBrow_UD", 0.000000f), new VisemeMapping("m_EmotionBrows_L", 0.000199f), new VisemeMapping("m_EmotionBrows_U", 0.321300f) } },
+ { "outBrow_left", new[] { new VisemeMapping("m_CockedBrows_D", 0.000000f), new VisemeMapping("m_EmotionBrows_D", 0.000000f), new VisemeMapping("m_CockedBrows_R", 0.129100f), new VisemeMapping("m_CockedBrows_L", -0.039700f), new VisemeMapping("m_CockedBrows_U", -0.000000f), new VisemeMapping("m_EmotionBrows_R", -0.000000f), new VisemeMapping("m_UpDownBrow_LR", 0.105200f), new VisemeMapping("m_UpDownBrow_UD", -0.000000f), new VisemeMapping("m_EmotionBrows_L", -0.000900f), new VisemeMapping("m_EmotionBrows_U", -0.094901f) } },
+ { "outBrow_Right", new[] { new VisemeMapping("m_CockedBrows_D", -0.000000f), new VisemeMapping("m_EmotionBrows_D", 0.000000f), new VisemeMapping("m_CockedBrows_R", -0.039500f), new VisemeMapping("m_CockedBrows_L", 0.129500f), new VisemeMapping("m_CockedBrows_U", 0.000000f), new VisemeMapping("m_EmotionBrows_R", 0.000000f), new VisemeMapping("m_UpDownBrow_LR", 0.105500f), new VisemeMapping("m_UpDownBrow_UD", 0.000000f), new VisemeMapping("m_EmotionBrows_L", -0.000900f), new VisemeMapping("m_EmotionBrows_U", -0.094600f) } },
+ { "underEye_left", new[] { new VisemeMapping("m_Closed_R", 0.405500f), new VisemeMapping("m_Closed_U", 0.171000f), new VisemeMapping("m_Open_UD", -0.467500f), new VisemeMapping("m_UpDownBrow_LR", 0.314700f), new VisemeMapping("m_Smile_Frown_U", 1.168500f), new VisemeMapping("m_Smile_Frown_D", 0.063800f), new VisemeMapping("m_Smile_Frown_L", 0.706201f), new VisemeMapping("m_Closed_L", 0.454300f), new VisemeMapping("m_Angry_L", 0.783999f) } },
+ { "underEye_Right", new[] { new VisemeMapping("m_Closed_R", 0.398400f), new VisemeMapping("m_Closed_U", 0.171300f), new VisemeMapping("m_Open_UD", -0.467200f), new VisemeMapping("m_UpDownBrow_LR", 0.315101f), new VisemeMapping("m_Smile_Frown_U", 1.168500f), new VisemeMapping("m_Smile_Frown_D", 0.063800f), new VisemeMapping("m_Smile_Frown_R", 0.706600f), new VisemeMapping("m_Closed_L", 0.454300f), new VisemeMapping("m_Angry_L", 0.224200f) } },
+ { "Sneer", new[] { new VisemeMapping("m_CockedBrows_D", -0.000001f), new VisemeMapping("m_EmotionBrows_D", -0.000001f), new VisemeMapping("m_CockedBrows_R", 0.000000f), new VisemeMapping("m_CockedBrows_L", 0.000000f), new VisemeMapping("m_CockedBrows_U", -0.000001f), new VisemeMapping("m_EmotionBrows_R", -0.001201f), new VisemeMapping("m_UpDownBrow_LR", 0.000000f), new VisemeMapping("m_UpDownBrow_UD", -0.002400f), new VisemeMapping("m_EmotionBrows_L", 0.005300f), new VisemeMapping("m_EmotionBrows_U", 0.823400f) } },
+ };
+
+ ///
+ /// All viseme animation names used in lip sync for Human Child
+ /// Similar to adult human visemes
+ ///
+ public static readonly string[] HumanChildVisemes =
+ [
+ // Head and neck rotations
+ "neck_RX+",
+ "neck_RX-",
+ "neck_RZ+",
+ "neck_RZ-",
+ "neck_RY+",
+ "neck_RY-",
+ "head_RX+",
+ "head_RX-",
+ "head_RY+",
+ "head_RY-",
+ "head_RZ+",
+ "head_RZ-",
+ // Brow controls
+ "m_CockedBrows_D",
+ "m_CockedBrows_R",
+ "m_CockedBrows_L",
+ "m_CockedBrows_U",
+ "m_EmotionBrows_D",
+ "m_EmotionBrows_R",
+ "m_EmotionBrows_L",
+ "m_EmotionBrows_U",
+ "m_UpDownBrow_LR",
+ "m_UpDownBrow_UD",
+ // Eyelid controls
+ "m_Squint_Eyelids_LR",
+ "m_Squint_Eyelids_UD",
+ "m_WideOpen_Eyelids_LR",
+ "m_WideOpen_Eyelids_UD",
+ "m_EyelidsLookat_L",
+ "m_EyelidsLookat_R",
+ "m_EyelidsLookat_U",
+ "m_EyelidsLookat_D",
+ "m_BlinksLookat_UD",
+ "m_BlinksLookat_LR",
+ // Jaw controls
+ "m_JawRotate_L",
+ "m_JawRotate_R",
+ "m_JawRotate_U",
+ "m_JawRotate_D",
+ "m_Jaw+",
+ "m_Jaw-",
+ "m_Open_UD",
+ "m_Open_LR",
+ "m_Open",
+ // Mouth phoneme shapes
+ "m_OH",
+ "m_EE",
+ "m_EH",
+ "m_OW",
+ "m_ZZ",
+ "m_TH",
+ "m_N",
+ "m_L",
+ "m_G",
+ "m_M",
+ "m_FV",
+ "m_Flap",
+ // Closed/Offset controls
+ "m_Closed_R",
+ "m_Closed_L",
+ "m_Closed_U",
+ "m_Closed_D",
+ "m_Offset_L",
+ "m_Offset_R",
+ "m_Offset_U",
+ "m_Offset_D",
+ // Expression controls
+ "m_Angry_UD",
+ "m_Angry_L",
+ "m_Angry_R",
+ "m_Smile_Frown_U",
+ "m_Smile_Frown_D",
+ "m_Smile_Frown_L",
+ "m_Smile_Frown_R",
+ "m_Sneer_UD",
+ "m_Sneer_LR",
+ // Eye rotations
+ "eye_Right_RX+",
+ "eye_Right_RX-",
+ "eye_Right_RY+",
+ "eye_Right_RY-",
+ "eye_Right_RZ+",
+ "eye_Right_RZ-",
+ "eye_Left_RX+",
+ "eye_Left_RX-",
+ "eye_Left_RY+",
+ "eye_Left_RY-",
+ "eye_Left_RZ+",
+ "eye_Left_RZ-"
+ ];
+
+ ///
+ /// Quarian phoneme to viseme mappings - from SFX_Quarian_FaceFX data.
+ /// Note: Quarians wear environmental suits with helmets, so their facial animations
+ /// are simplified to primarily use "jawOpen" for all phonemes visible through the visor.
+ ///
+ public static readonly Dictionary QuarianPhonemeMap = new()
+ {
+ // Quarian phoneme mappings from SFX_Quarian_FaceFX
+ // All mappings use jawOpen since the face is behind a helmet visor
+ { "brow_left", new[] { new VisemeMapping("jawOpen", 0.524650f) } },
+ { "brow_right", new[] { new VisemeMapping("jawOpen", 0.719121f) } },
+ { "cheek_left", new[] { new VisemeMapping("jawOpen", 0.908336f) } },
+ { "cheek_right", new[] { new VisemeMapping("jawOpen", 0.529906f) } },
+ { "Chest", new[] { new VisemeMapping("jawOpen", 0.093661f) } },
+ { "Chest1", new[] { new VisemeMapping("jawOpen", 0.834752f) } },
+ { "Chest2", new[] { new VisemeMapping("jawOpen", 0.734889f) } },
+ { "eye_Left", new[] { new VisemeMapping("jawOpen", 0.387995f) } },
+ { "eye_Right", new[] { new VisemeMapping("jawOpen", 0.750657f) } },
+ { "eyeBlink_Left", new[] { new VisemeMapping("jawOpen", 0.345948f) } },
+ { "eyeBlink_Right", new[] { new VisemeMapping("jawOpen", 0.866288f) } },
+ { "GOD", new[] { new VisemeMapping("jawOpen", 0.529906f) } },
+ { "Head", new[] { new VisemeMapping("jawOpen", 0.424787f) } },
+ { "HeadBase", new[] { new VisemeMapping("jawOpen", 0.309156f) } },
+ { "innerLowLip_left", new[] { new VisemeMapping("jawOpen", 0.298644f) } },
+ { "innerLowLip_right", new[] { new VisemeMapping("jawOpen", 0.514139f) } },
+ { "innerUpperLip_left", new[] { new VisemeMapping("jawOpen", 0.734889f) } },
+ { "innerUpperLip_right", new[] { new VisemeMapping("jawOpen", 0.219804f) } },
+ { "jawBone", new[] { new VisemeMapping("jawOpen", 0.650794f) } },
+ { "LeftCollar", new[] { new VisemeMapping("jawOpen", 0.687585f) } },
+ { "LeftElbow", new[] { new VisemeMapping("jawOpen", 0.666561f) } },
+ { "LeftElbowTwist1", new[] { new VisemeMapping("jawOpen", 0.571954f) } },
+ { "LeftIndexFinger", new[] { new VisemeMapping("jawOpen", 0.645538f) } },
+ { "LeftIndexFinger1", new[] { new VisemeMapping("jawOpen", 0.430043f) } },
+ { "LeftIndexFinger2", new[] { new VisemeMapping("jawOpen", 0.629770f) } },
+ { "LeftMiddleFinger", new[] { new VisemeMapping("jawOpen", 0.719121f) } },
+ { "LeftMiddleFinger1", new[] { new VisemeMapping("jawOpen", 0.477347f) } },
+ { "LeftMiddleFinger2", new[] { new VisemeMapping("jawOpen", 0.677073f) } },
+ { "LeftPinkFinger", new[] { new VisemeMapping("jawOpen", 0.724377f) } },
+ { "LeftPinkFinger1", new[] { new VisemeMapping("jawOpen", 0.924104f) } },
+ { "LeftPinkFinger2", new[] { new VisemeMapping("jawOpen", 0.955640f) } },
+ { "LeftRingFinger", new[] { new VisemeMapping("jawOpen", 0.393251f) } },
+ { "LeftRingFinger1", new[] { new VisemeMapping("jawOpen", 0.959360f) } },
+ { "LeftRingFinger2", new[] { new VisemeMapping("jawOpen", 0.871544f) } },
+ { "LeftShoulder", new[] { new VisemeMapping("jawOpen", 0.656050f) } },
+ { "LeftThumbFinger", new[] { new VisemeMapping("jawOpen", 0.303900f) } },
+ { "LeftThumbFinger1", new[] { new VisemeMapping("jawOpen", 0.193525f) } },
+ { "LeftThumbFinger2", new[] { new VisemeMapping("jawOpen", 0.582466f) } },
+ { "LeftWrist", new[] { new VisemeMapping("jawOpen", 0.824240f) } },
+ { "LipCorner_left", new[] { new VisemeMapping("jawOpen", 0.472091f) } },
+ { "LipCorner_right", new[] { new VisemeMapping("jawOpen", 0.324924f) } },
+ { "LowerBack", new[] { new VisemeMapping("jawOpen", 0.256596f) } },
+ { "LowerCheek_left", new[] { new VisemeMapping("jawOpen", 0.298644f) } },
+ { "lowerCheek_right", new[] { new VisemeMapping("jawOpen", 0.924104f) } },
+ { "lowerLip_left", new[] { new VisemeMapping("jawOpen", 0.850520f) } },
+ { "lowerLip_right", new[] { new VisemeMapping("jawOpen", 0.083149f) } },
+ { "lowLid_Left", new[] { new VisemeMapping("jawOpen", 0.708609f) } },
+ { "lowLid_Right", new[] { new VisemeMapping("jawOpen", 0.508883f) } },
+ { "MouthBase", new[] { new VisemeMapping("jawOpen", 0.729633f) } },
+ { "Neck", new[] { new VisemeMapping("jawOpen", 1.000000f) } },
+ { "Neck1", new[] { new VisemeMapping("jawOpen", 0.535162f) } },
+ { "outBrow_left", new[] { new VisemeMapping("jawOpen", 0.703353f) } },
+ { "outBrow_Right", new[] { new VisemeMapping("jawOpen", 0.939872f) } },
+ { "outerUpperLip_left", new[] { new VisemeMapping("jawOpen", 0.324924f) } },
+ { "outerUpperLip_right", new[] { new VisemeMapping("jawOpen", 0.761169f) } },
+ { "Prop02", new[] { new VisemeMapping("jawOpen", 0.629770f) } },
+ { "Root", new[] { new VisemeMapping("jawOpen", 0.818985f) } },
+ { "SFX_Quarian", new[] { new VisemeMapping("jawOpen", 0.014822f) } },
+ { "sneer", new[] { new VisemeMapping("jawOpen", 0.508883f) } },
+ { "Socket_02", new[] { new VisemeMapping("jawOpen", 0.603490f) } },
+ { "tongue", new[] { new VisemeMapping("jawOpen", 0.903080f) } },
+ { "underEye_left", new[] { new VisemeMapping("jawOpen", 0.409019f) } },
+ { "underEye_Right", new[] { new VisemeMapping("jawOpen", 0.277620f) } },
+ { "upperLip_left", new[] { new VisemeMapping("jawOpen", 0.876800f) } },
+ { "upperLip_right", new[] { new VisemeMapping("jawOpen", 0.866288f) } },
+ };
+
+ ///
+ /// All viseme animation names used in lip sync for Quarian
+ /// Note: Quarians use a simplified system with only jawOpen due to wearing helmets
+ ///
+ public static readonly string[] QuarianVisemes =
+ [
+ // Quarians primarily use jawOpen for all facial animations
+ // since their face is behind a helmet visor
+ "jawOpen"
+ ];
+
+ ///
+ /// Geth phoneme to viseme mappings - from SFX_Legion_FaceFX data.
+ /// Note: Geth (like Legion) communicate through their eye/lamp "blinker" and
+ /// head plate movements rather than traditional mouth movements.
+ /// Standard phonemes are mapped to blinker, head orientation, gaze, and gesture animations.
+ ///
+ public static readonly Dictionary GethPhonemeMap = new()
+ {
+ // Geth use a "blinker" system - their eye lamp flickers when speaking
+ // They also have head/gaze movements and gesture animations during speech
+
+ // Silence - minimal activity, slight idle movement
+ { "SIL", new[]
+ {
+ new VisemeMapping("blinker", 0.1f),
+ new VisemeMapping("G_TalkingNormal", 0.05f),
+ new VisemeMapping("E_defaultNoiseLoop", 0.1f),
+ }},
+
+ // Vowels - high activity across all animations
+ { "AA", new[]
+ {
+ new VisemeMapping("blinker", 0.9f),
+ new VisemeMapping("G_TalkingNormal", 0.8f),
+ new VisemeMapping("E_defaultNoiseLoop", 0.3f),
+ new VisemeMapping("Emphasis_Head_Pitch", 0.4f),
+ new VisemeMapping("Emphasis_Head_Yaw", 0.2f),
+ }},
+ { "AE", new[]
+ {
+ new VisemeMapping("blinker", 0.85f),
+ new VisemeMapping("G_TalkingNormal", 0.75f),
+ new VisemeMapping("E_defaultNoiseLoop", 0.25f),
+ new VisemeMapping("Emphasis_Head_Pitch", 0.35f),
+ }},
+ { "AH", new[]
+ {
+ new VisemeMapping("blinker", 0.8f),
+ new VisemeMapping("G_TalkingNormal", 0.7f),
+ new VisemeMapping("E_defaultNoiseLoop", 0.2f),
+ new VisemeMapping("Emphasis_Head_Pitch", 0.3f),
+ }},
+ { "AO", new[]
+ {
+ new VisemeMapping("blinker", 0.85f),
+ new VisemeMapping("G_TalkingNormal", 0.75f),
+ new VisemeMapping("E_defaultNoiseLoop", 0.25f),
+ new VisemeMapping("E_Neutral_Thoughtfull", 0.2f),
+ }},
+ { "AW", new[]
+ {
+ new VisemeMapping("blinker", 0.9f),
+ new VisemeMapping("G_TalkingNormal", 0.8f),
+ new VisemeMapping("E_defaultNoiseLoop", 0.3f),
+ new VisemeMapping("Emphasis_Head_Roll", 0.2f),
+ }},
+ { "AY", new[]
+ {
+ new VisemeMapping("blinker", 0.9f),
+ new VisemeMapping("G_TalkingNormal", 0.8f),
+ new VisemeMapping("E_defaultNoiseLoop", 0.3f),
+ new VisemeMapping("Gaze_Eye_Pitch", 0.15f),
+ }},
+ { "EH", new[]
+ {
+ new VisemeMapping("blinker", 0.8f),
+ new VisemeMapping("G_TalkingNormal", 0.7f),
+ new VisemeMapping("E_defaultNoiseLoop", 0.2f),
+ }},
+ { "ER", new[]
+ {
+ new VisemeMapping("blinker", 0.75f),
+ new VisemeMapping("G_TalkingNormal", 0.65f),
+ new VisemeMapping("E_defaultNoiseLoop", 0.2f),
+ new VisemeMapping("E_Neutral_Thoughtfull", 0.15f),
+ }},
+ { "EY", new[]
+ {
+ new VisemeMapping("blinker", 0.85f),
+ new VisemeMapping("G_TalkingNormal", 0.75f),
+ new VisemeMapping("E_defaultNoiseLoop", 0.25f),
+ new VisemeMapping("Gaze_Eye_Yaw", 0.1f),
+ }},
+ { "IH", new[]
+ {
+ new VisemeMapping("blinker", 0.75f),
+ new VisemeMapping("G_TalkingNormal", 0.65f),
+ new VisemeMapping("E_defaultNoiseLoop", 0.2f),
+ }},
+ { "IY", new[]
+ {
+ new VisemeMapping("blinker", 0.8f),
+ new VisemeMapping("G_TalkingNormal", 0.7f),
+ new VisemeMapping("E_defaultNoiseLoop", 0.2f),
+ new VisemeMapping("Emphasis_Head_Pitch", 0.25f),
+ }},
+ { "OW", new[]
+ {
+ new VisemeMapping("blinker", 0.85f),
+ new VisemeMapping("G_TalkingNormal", 0.75f),
+ new VisemeMapping("E_defaultNoiseLoop", 0.25f),
+ new VisemeMapping("E_GESTURE_HeadLeft", 0.15f),
+ }},
+ { "OY", new[]
+ {
+ new VisemeMapping("blinker", 0.9f),
+ new VisemeMapping("G_TalkingNormal", 0.8f),
+ new VisemeMapping("E_defaultNoiseLoop", 0.3f),
+ new VisemeMapping("Emphasis_Head_Yaw", 0.2f),
+ }},
+ { "UH", new[]
+ {
+ new VisemeMapping("blinker", 0.7f),
+ new VisemeMapping("G_TalkingNormal", 0.6f),
+ new VisemeMapping("E_defaultNoiseLoop", 0.15f),
+ }},
+ { "UW", new[]
+ {
+ new VisemeMapping("blinker", 0.75f),
+ new VisemeMapping("G_TalkingNormal", 0.65f),
+ new VisemeMapping("E_defaultNoiseLoop", 0.2f),
+ new VisemeMapping("E_GESTURE_NeckForwardLeft", 0.1f),
+ }},
+
+ // Consonants - varying activity levels
+ // Stops (plosives) - quick burst of activity
+ { "B", new[]
+ {
+ new VisemeMapping("blinker", 0.7f),
+ new VisemeMapping("G_TalkingNormal", 0.6f),
+ new VisemeMapping("E_defaultNoiseLoop", 0.15f),
+ new VisemeMapping("Emphasis_Head_Pitch", 0.2f),
+ }},
+ { "D", new[]
+ {
+ new VisemeMapping("blinker", 0.65f),
+ new VisemeMapping("G_TalkingNormal", 0.55f),
+ new VisemeMapping("E_defaultNoiseLoop", 0.15f),
+ }},
+ { "G", new[]
+ {
+ new VisemeMapping("blinker", 0.65f),
+ new VisemeMapping("G_TalkingNormal", 0.55f),
+ new VisemeMapping("E_defaultNoiseLoop", 0.15f),
+ new VisemeMapping("E_GESTURE_HeadRollLeft", 0.1f),
+ }},
+ { "K", new[]
+ {
+ new VisemeMapping("blinker", 0.6f),
+ new VisemeMapping("G_TalkingNormal", 0.5f),
+ new VisemeMapping("E_defaultNoiseLoop", 0.1f),
+ }},
+ { "P", new[]
+ {
+ new VisemeMapping("blinker", 0.6f),
+ new VisemeMapping("G_TalkingNormal", 0.5f),
+ new VisemeMapping("E_defaultNoiseLoop", 0.1f),
+ }},
+ { "T", new[]
+ {
+ new VisemeMapping("blinker", 0.6f),
+ new VisemeMapping("G_TalkingNormal", 0.5f),
+ new VisemeMapping("E_defaultNoiseLoop", 0.1f),
+ }},
+
+ // Fricatives - sustained activity
+ { "CH", new[]
+ {
+ new VisemeMapping("blinker", 0.7f),
+ new VisemeMapping("G_TalkingNormal", 0.6f),
+ new VisemeMapping("E_defaultNoiseLoop", 0.2f),
+ new VisemeMapping("Emphasis_Head_Roll", 0.15f),
+ }},
+ { "DH", new[]
+ {
+ new VisemeMapping("blinker", 0.65f),
+ new VisemeMapping("G_TalkingNormal", 0.55f),
+ new VisemeMapping("E_defaultNoiseLoop", 0.15f),
+ }},
+ { "F", new[]
+ {
+ new VisemeMapping("blinker", 0.5f),
+ new VisemeMapping("G_TalkingNormal", 0.4f),
+ new VisemeMapping("E_defaultNoiseLoop", 0.1f),
+ }},
+ { "HH", new[]
+ {
+ new VisemeMapping("blinker", 0.4f),
+ new VisemeMapping("G_TalkingNormal", 0.3f),
+ new VisemeMapping("E_defaultNoiseLoop", 0.1f),
+ new VisemeMapping("E_Neutral_Thoughtfull", 0.1f),
+ }},
+ { "JH", new[]
+ {
+ new VisemeMapping("blinker", 0.7f),
+ new VisemeMapping("G_TalkingNormal", 0.6f),
+ new VisemeMapping("E_defaultNoiseLoop", 0.2f),
+ }},
+ { "S", new[]
+ {
+ new VisemeMapping("blinker", 0.55f),
+ new VisemeMapping("G_TalkingNormal", 0.45f),
+ new VisemeMapping("E_defaultNoiseLoop", 0.15f),
+ }},
+ { "SH", new[]
+ {
+ new VisemeMapping("blinker", 0.6f),
+ new VisemeMapping("G_TalkingNormal", 0.5f),
+ new VisemeMapping("E_defaultNoiseLoop", 0.15f),
+ }},
+ { "TH", new[]
+ {
+ new VisemeMapping("blinker", 0.5f),
+ new VisemeMapping("G_TalkingNormal", 0.4f),
+ new VisemeMapping("E_defaultNoiseLoop", 0.1f),
+ }},
+ { "V", new[]
+ {
+ new VisemeMapping("blinker", 0.6f),
+ new VisemeMapping("G_TalkingNormal", 0.5f),
+ new VisemeMapping("E_defaultNoiseLoop", 0.15f),
+ }},
+ { "Z", new[]
+ {
+ new VisemeMapping("blinker", 0.65f),
+ new VisemeMapping("G_TalkingNormal", 0.55f),
+ new VisemeMapping("E_defaultNoiseLoop", 0.2f),
+ }},
+ { "ZH", new[]
+ {
+ new VisemeMapping("blinker", 0.65f),
+ new VisemeMapping("G_TalkingNormal", 0.55f),
+ new VisemeMapping("E_defaultNoiseLoop", 0.2f),
+ }},
+
+ // Nasals - moderate activity
+ { "M", new[]
+ {
+ new VisemeMapping("blinker", 0.6f),
+ new VisemeMapping("G_TalkingNormal", 0.5f),
+ new VisemeMapping("E_defaultNoiseLoop", 0.15f),
+ new VisemeMapping("E_GESTURE_NeckBackLeft", 0.1f),
+ }},
+ { "N", new[]
+ {
+ new VisemeMapping("blinker", 0.55f),
+ new VisemeMapping("G_TalkingNormal", 0.45f),
+ new VisemeMapping("E_defaultNoiseLoop", 0.15f),
+ }},
+ { "NG", new[]
+ {
+ new VisemeMapping("blinker", 0.55f),
+ new VisemeMapping("G_TalkingNormal", 0.45f),
+ new VisemeMapping("E_defaultNoiseLoop", 0.15f),
+ }},
+
+ // Liquids and glides - smooth activity
+ { "L", new[]
+ {
+ new VisemeMapping("blinker", 0.6f),
+ new VisemeMapping("G_TalkingNormal", 0.5f),
+ new VisemeMapping("E_defaultNoiseLoop", 0.15f),
+ }},
+ { "R", new[]
+ {
+ new VisemeMapping("blinker", 0.65f),
+ new VisemeMapping("G_TalkingNormal", 0.55f),
+ new VisemeMapping("E_defaultNoiseLoop", 0.2f),
+ new VisemeMapping("E_Neutral_Thoughtfull", 0.1f),
+ }},
+ { "W", new[]
+ {
+ new VisemeMapping("blinker", 0.6f),
+ new VisemeMapping("G_TalkingNormal", 0.5f),
+ new VisemeMapping("E_defaultNoiseLoop", 0.15f),
+ }},
+ { "Y", new[]
+ {
+ new VisemeMapping("blinker", 0.6f),
+ new VisemeMapping("G_TalkingNormal", 0.5f),
+ new VisemeMapping("E_defaultNoiseLoop", 0.15f),
+ new VisemeMapping("Gaze_Eye_Pitch", 0.1f),
+ }},
+ };
+
+ ///
+ /// All viseme animation names used in lip sync for Geth
+ /// Note: Geth use a "blinker" system plus head/gaze movements and gestures.
+ ///
+ public static readonly string[] GethVisemes =
+ [
+ // Primary speech animation - the eye lamp blinker
+ "blinker",
+ // Standard expression/orientation animations
+ "Orientation_Head_Pitch",
+ "Orientation_Head_Roll",
+ "Orientation_Head_Yaw",
+ "Gaze_Eye_Pitch",
+ "Gaze_Eye_Yaw",
+ "Emphasis_Head_Pitch",
+ "Emphasis_Head_Roll",
+ "Emphasis_Head_Yaw",
+ "Eyebrow_Raise",
+ "Blink",
+ // Geth-specific gesture animations
+ "G_TalkingNormal",
+ "E_defaultNoiseLoop",
+ "E_Neutral_Thoughtfull",
+ "E_GESTURE_HeadRollLeft",
+ "E_GESTURE_HeadLeft",
+ "E_GESTURE_NeckForwardLeft",
+ "E_GESTURE_NeckBackLeft"
+ ];
+
+
+ ///
+ /// Non-lip sync animations that control head/eye movement
+ ///
+ public static readonly string[] ExpressionAnimations =
+ [
+ "Orientation_Head_Pitch",
+ "Orientation_Head_Roll",
+ "Orientation_Head_Yaw",
+ "Gaze_Eye_Pitch",
+ "Gaze_Eye_Yaw",
+ "Emphasis_Head_Pitch",
+ "Emphasis_Head_Roll",
+ "Emphasis_Head_Yaw",
+ "Eyebrow_Raise",
+ "Blink"
+ ];
+ }
+
+ ///
+ /// Represents a mapping from a phoneme to a viseme animation
+ ///
+ public class VisemeMapping
+ {
+ public string VisemeName { get; }
+ public float Weight { get; }
+
+ public VisemeMapping(string visemeName, float weight)
+ {
+ VisemeName = visemeName;
+ Weight = weight;
+ }
+ }
+}
diff --git a/LegendaryExplorer/LegendaryExplorer/LegendaryExplorer/Tools/FaceFXEditor/AutoFaceFXGenerator/TextToPhonemeAnalyzer.cs b/LegendaryExplorer/LegendaryExplorer/LegendaryExplorer/Tools/FaceFXEditor/AutoFaceFXGenerator/TextToPhonemeAnalyzer.cs
new file mode 100644
index 0000000000..8de64e777c
--- /dev/null
+++ b/LegendaryExplorer/LegendaryExplorer/LegendaryExplorer/Tools/FaceFXEditor/AutoFaceFXGenerator/TextToPhonemeAnalyzer.cs
@@ -0,0 +1,254 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.RegularExpressions;
+
+namespace LegendaryExplorer.Tools.FaceFXEditor.AutoFaceFXGenerator
+{
+ ///
+ /// Analyzes text and converts it to phonemes for lip sync generation
+ ///
+ public static class TextToPhonemeAnalyzer
+ {
+ ///
+ /// Simple grapheme-to-phoneme mapping for English
+ /// This is a simplified approach; more sophisticated systems would use CMU dict or ML models
+ ///
+ private static readonly Dictionary GraphemeToPhoneme = new()
+ {
+ // Common letter combinations (order matters - check longer combinations first)
+ { "tion", new[] { "SH", "AH", "N" } },
+ { "sion", new[] { "ZH", "AH", "N" } },
+ { "ough", new[] { "AH", "F" } },
+ { "ight", new[] { "AY", "T" } },
+ { "eigh", new[] { "EY" } },
+ { "ould", new[] { "UH", "D" } },
+ { "ally", new[] { "AE", "L", "IY" } },
+ { "illy", new[] { "IH", "L", "IY" } },
+ { "ully", new[] { "UH", "L", "IY" } },
+ { "all", new[] { "AO", "L" } },
+ { "ell", new[] { "EH", "L" } },
+ { "ill", new[] { "IH", "L" } },
+ { "oll", new[] { "AA", "L" } },
+ { "ull", new[] { "UH", "L" } },
+ { "ll", new[] { "L" } },
+ { "le", new[] { "AH", "L" } },
+ { "th", new[] { "TH" } },
+ { "ch", new[] { "CH" } },
+ { "sh", new[] { "SH" } },
+ { "ph", new[] { "F" } },
+ { "wh", new[] { "W" } },
+ { "ng", new[] { "NG" } },
+ { "qu", new[] { "K", "W" } },
+ { "ck", new[] { "K" } },
+ { "ee", new[] { "IY" } },
+ { "ea", new[] { "IY" } },
+ { "oo", new[] { "UW" } },
+ { "ou", new[] { "AW" } },
+ { "ow", new[] { "OW" } },
+ { "oi", new[] { "OY" } },
+ { "oy", new[] { "OY" } },
+ { "ai", new[] { "EY" } },
+ { "ay", new[] { "EY" } },
+ { "au", new[] { "AO" } },
+ { "aw", new[] { "AO" } },
+ { "ie", new[] { "IY" } },
+ { "ue", new[] { "UW" } },
+ { "er", new[] { "ER" } },
+ { "ir", new[] { "ER" } },
+ { "ur", new[] { "ER" } },
+ { "or", new[] { "AO", "R" } },
+ { "ar", new[] { "AA", "R" } },
+
+ // Single letters
+ { "a", new[] { "AE" } },
+ { "b", new[] { "B" } },
+ { "c", new[] { "K" } }, // simplified
+ { "d", new[] { "D" } },
+ { "e", new[] { "EH" } },
+ { "f", new[] { "F" } },
+ { "g", new[] { "G" } },
+ { "h", new[] { "H" } },
+ { "i", new[] { "IH" } },
+ { "j", new[] { "JH" } },
+ { "k", new[] { "K" } },
+ { "l", new[] { "L" } },
+ { "m", new[] { "M" } },
+ { "n", new[] { "N" } },
+ { "o", new[] { "AA" } },
+ { "p", new[] { "P" } },
+ { "q", new[] { "K" } },
+ { "r", new[] { "R" } },
+ { "s", new[] { "S" } },
+ { "t", new[] { "T" } },
+ { "u", new[] { "AH" } },
+ { "v", new[] { "V" } },
+ { "w", new[] { "W" } },
+ { "x", new[] { "K", "S" } },
+ { "y", new[] { "Y" } },
+ { "z", new[] { "Z" } },
+ };
+
+ ///
+ /// Converts text to a list of phonemes with timing information
+ ///
+ /// The text to analyze
+ /// Total duration in seconds
+ /// List of phonemes with their start times and durations
+ public static List AnalyzeText(string text, float duration)
+ {
+ var result = new List();
+
+ if (string.IsNullOrWhiteSpace(text))
+ return result;
+
+ // Clean the text - remove punctuation but keep spaces
+ text = Regex.Replace(text.ToLower(), @"[^\w\s]", "");
+
+ // Get all phonemes from the text
+ var allPhonemes = new List();
+ int i = 0;
+ while (i < text.Length)
+ {
+ if (char.IsWhiteSpace(text[i]))
+ {
+ // Add a pause phoneme for spaces between words
+ allPhonemes.Add("PAUSE");
+ i++;
+ continue;
+ }
+
+ bool found = false;
+
+ // Try matching longer sequences first (up to 5 characters)
+ for (int len = Math.Min(5, text.Length - i); len > 0; len--)
+ {
+ string substr = text.Substring(i, len);
+ if (GraphemeToPhoneme.TryGetValue(substr, out string[] phonemes))
+ {
+ allPhonemes.AddRange(phonemes);
+ i += len;
+ found = true;
+ break;
+ }
+ }
+
+ if (!found)
+ {
+ // Skip unknown characters
+ i++;
+ }
+ }
+
+ if (allPhonemes.Count == 0)
+ return result;
+
+ // Calculate timing for each phoneme
+ // Use natural speaking rhythm - average phoneme is about 60-80ms
+ // But scale to fit the actual audio duration
+
+ float totalWeight = allPhonemes.Sum(p => GetPhonemeWeight(p));
+ float availableDuration = duration - 0.1f; // Leave small buffer at start/end
+
+ // Calculate minimum phoneme duration based on natural speech
+ // Natural speech is about 12-15 phonemes per second (faster rate for snappier lip sync)
+ float naturalPhonemeRate = 14f; // phonemes per second (increased from 12)
+ float naturalDuration = allPhonemes.Count / naturalPhonemeRate;
+
+ // If the audio is longer than natural speech would take,
+ // stretch phonemes but don't make them unnaturally long
+ float timeScale = availableDuration / Math.Max(naturalDuration, 0.1f);
+ timeScale = Math.Max(0.6f, Math.Min(timeScale, 2.5f)); // Clamp between 0.6x and 2.5x natural speed
+
+ float currentTime = 0.05f; // Small offset from the start
+
+ foreach (var phoneme in allPhonemes)
+ {
+ float weight = GetPhonemeWeight(phoneme);
+
+ // Base duration from natural speech timing
+ float baseDuration = (weight / naturalPhonemeRate) * timeScale;
+
+ // Ensure minimum duration for visibility (at least 50ms for faster transitions)
+ float phonemeDuration = Math.Max(baseDuration, 0.05f);
+
+ if (phoneme != "PAUSE")
+ {
+ result.Add(new PhonemeData
+ {
+ Phoneme = phoneme,
+ StartTime = currentTime,
+ Duration = phonemeDuration
+ });
+ }
+
+ currentTime += phonemeDuration;
+
+ // If we're running past the audio duration, stop adding phonemes
+ if (currentTime >= duration - 0.05f)
+ break;
+ }
+
+ return result;
+ }
+
+ ///
+ /// Gets the weight of a phoneme for timing calculation
+ /// Vowels generally take longer to pronounce than consonants
+ ///
+ private static float GetPhonemeWeight(string phoneme)
+ {
+ if (phoneme == "PAUSE")
+ return 0.3f; // Pauses are short
+
+ // Vowels and diphthongs take longer
+ var longVowels = new HashSet
+ {
+ "IY", "EY", "UW", "OW", "AO", "AA", "AY", "AW", "OY"
+ };
+
+ if (longVowels.Contains(phoneme))
+ return 1.8f;
+
+ // Short vowels
+ var shortVowels = new HashSet
+ {
+ "IH", "EH", "AE", "AH", "ER", "UH"
+ };
+
+ if (shortVowels.Contains(phoneme))
+ return 1.2f;
+
+ // Stops are very short
+ var stops = new HashSet
+ {
+ "P", "B", "T", "D", "K", "G"
+ };
+
+ if (stops.Contains(phoneme))
+ return 0.5f;
+
+ // Fricatives are medium
+ var fricatives = new HashSet
+ {
+ "F", "V", "S", "Z", "SH", "ZH", "TH", "DH", "H"
+ };
+
+ if (fricatives.Contains(phoneme))
+ return 0.8f;
+
+ return 1.0f; // Default for other consonants
+ }
+ }
+
+ ///
+ /// Represents a phoneme with timing information
+ ///
+ public class PhonemeData
+ {
+ public string Phoneme { get; set; }
+ public float StartTime { get; set; }
+ public float Duration { get; set; }
+ public float EndTime => StartTime + Duration;
+ }
+}
diff --git a/LegendaryExplorer/LegendaryExplorer/LightTheme.xaml b/LegendaryExplorer/LegendaryExplorer/LightTheme.xaml
new file mode 100644
index 0000000000..865f627c4a
--- /dev/null
+++ b/LegendaryExplorer/LegendaryExplorer/LightTheme.xaml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/LegendaryExplorer/LegendaryExplorer/MainWindow/About.xaml b/LegendaryExplorer/LegendaryExplorer/MainWindow/About.xaml
index 68d9cf9f06..51572ae996 100644
--- a/LegendaryExplorer/LegendaryExplorer/MainWindow/About.xaml
+++ b/LegendaryExplorer/LegendaryExplorer/MainWindow/About.xaml
@@ -6,7 +6,7 @@
xmlns:local="clr-namespace:LegendaryExplorer"
xmlns:misc="clr-namespace:LegendaryExplorer.Misc"
mc:Ignorable="d"
- Title="About LegendaryExplorer" ResizeMode="CanMinimize" Background="#FFD8D8D8" WindowStartupLocation="CenterOwner"
+ Title="About LegendaryExplorer" ResizeMode="CanMinimize" Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}" WindowStartupLocation="CenterOwner"
Width="616" SizeToContent="Height">
@@ -20,14 +20,14 @@
-
+
-
+
-
-
+
@@ -47,24 +47,24 @@
-
+
+
+
+
+
diff --git a/LegendaryExplorer/LegendaryExplorer/SharedUI/Controls/WatermarkComboBox.xaml.cs b/LegendaryExplorer/LegendaryExplorer/SharedUI/Controls/WatermarkComboBox.xaml.cs
new file mode 100644
index 0000000000..ff53362134
--- /dev/null
+++ b/LegendaryExplorer/LegendaryExplorer/SharedUI/Controls/WatermarkComboBox.xaml.cs
@@ -0,0 +1,145 @@
+using System;
+using System.Collections;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+
+namespace LegendaryExplorer.SharedUI.Controls
+{
+ ///
+ /// A ComboBox with watermark/placeholder text that respects theme settings.
+ ///
+ public partial class WatermarkComboBox : UserControl
+ {
+ public WatermarkComboBox()
+ {
+ InitializeComponent();
+ }
+
+ #region Dependency Properties
+
+ public static readonly DependencyProperty WatermarkProperty =
+ DependencyProperty.Register(nameof(Watermark), typeof(string), typeof(WatermarkComboBox),
+ new PropertyMetadata(string.Empty));
+
+ public string Watermark
+ {
+ get => (string)GetValue(WatermarkProperty);
+ set => SetValue(WatermarkProperty, value);
+ }
+
+ public static readonly DependencyProperty ItemsSourceProperty =
+ DependencyProperty.Register(nameof(ItemsSource), typeof(IEnumerable), typeof(WatermarkComboBox),
+ new PropertyMetadata(null));
+
+ public IEnumerable ItemsSource
+ {
+ get => (IEnumerable)GetValue(ItemsSourceProperty);
+ set => SetValue(ItemsSourceProperty, value);
+ }
+
+ public static readonly DependencyProperty SelectedItemProperty =
+ DependencyProperty.Register(nameof(SelectedItem), typeof(object), typeof(WatermarkComboBox),
+ new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
+
+ public object SelectedItem
+ {
+ get => GetValue(SelectedItemProperty);
+ set => SetValue(SelectedItemProperty, value);
+ }
+
+ public static readonly DependencyProperty SelectedIndexProperty =
+ DependencyProperty.Register(nameof(SelectedIndex), typeof(int), typeof(WatermarkComboBox),
+ new FrameworkPropertyMetadata(-1, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
+
+ public int SelectedIndex
+ {
+ get => (int)GetValue(SelectedIndexProperty);
+ set => SetValue(SelectedIndexProperty, value);
+ }
+
+ public static readonly DependencyProperty IsTextSearchEnabledProperty =
+ DependencyProperty.Register(nameof(IsTextSearchEnabled), typeof(bool), typeof(WatermarkComboBox),
+ new PropertyMetadata(true));
+
+ public bool IsTextSearchEnabled
+ {
+ get => (bool)GetValue(IsTextSearchEnabledProperty);
+ set => SetValue(IsTextSearchEnabledProperty, value);
+ }
+
+ public static readonly DependencyProperty VerticalContentAlignmentProperty =
+ DependencyProperty.Register(nameof(VerticalContentAlignment), typeof(VerticalAlignment), typeof(WatermarkComboBox),
+ new PropertyMetadata(VerticalAlignment.Center));
+
+ public new VerticalAlignment VerticalContentAlignment
+ {
+ get => (VerticalAlignment)GetValue(VerticalContentAlignmentProperty);
+ set => SetValue(VerticalContentAlignmentProperty, value);
+ }
+
+ public static readonly DependencyProperty ItemsPanelProperty =
+ DependencyProperty.Register(nameof(ItemsPanel), typeof(ItemsPanelTemplate), typeof(WatermarkComboBox),
+ new PropertyMetadata(null, OnItemsPanelChanged));
+
+ public ItemsPanelTemplate ItemsPanel
+ {
+ get => (ItemsPanelTemplate)GetValue(ItemsPanelProperty);
+ set => SetValue(ItemsPanelProperty, value);
+ }
+
+ private static void OnItemsPanelChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ if (d is WatermarkComboBox control && e.NewValue is ItemsPanelTemplate template)
+ {
+ control.PART_ComboBox.ItemsPanel = template;
+ }
+ }
+
+ #endregion
+
+ #region Events
+
+ public new event KeyEventHandler KeyUp
+ {
+ add => PART_ComboBox.KeyUp += value;
+ remove => PART_ComboBox.KeyUp -= value;
+ }
+
+ public new event KeyEventHandler KeyDown
+ {
+ add => PART_ComboBox.KeyDown += value;
+ remove => PART_ComboBox.KeyDown -= value;
+ }
+
+ public event SelectionChangedEventHandler SelectionChanged
+ {
+ add => PART_ComboBox.SelectionChanged += value;
+ remove => PART_ComboBox.SelectionChanged -= value;
+ }
+
+ #endregion
+
+ #region Event Handlers
+
+ private void PART_ComboBox_Loaded(object sender, RoutedEventArgs e)
+ {
+ // Apply ItemsPanel if set
+ if (ItemsPanel != null)
+ {
+ PART_ComboBox.ItemsPanel = ItemsPanel;
+ }
+ }
+
+ #endregion
+
+ #region Methods
+
+ public new void Focus()
+ {
+ PART_ComboBox.Focus();
+ }
+
+ #endregion
+ }
+}
diff --git a/LegendaryExplorer/LegendaryExplorer/SharedUI/Controls/WatermarkTextBox.xaml b/LegendaryExplorer/LegendaryExplorer/SharedUI/Controls/WatermarkTextBox.xaml
new file mode 100644
index 0000000000..029ce24d44
--- /dev/null
+++ b/LegendaryExplorer/LegendaryExplorer/SharedUI/Controls/WatermarkTextBox.xaml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/LegendaryExplorer/LegendaryExplorer/SharedUI/Controls/WatermarkTextBox.xaml.cs b/LegendaryExplorer/LegendaryExplorer/SharedUI/Controls/WatermarkTextBox.xaml.cs
new file mode 100644
index 0000000000..6b9e6c9ca8
--- /dev/null
+++ b/LegendaryExplorer/LegendaryExplorer/SharedUI/Controls/WatermarkTextBox.xaml.cs
@@ -0,0 +1,103 @@
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+
+namespace LegendaryExplorer.SharedUI.Controls
+{
+ ///
+ /// A TextBox with watermark/placeholder text that respects theme settings.
+ ///
+ public partial class WatermarkTextBox : UserControl
+ {
+ public WatermarkTextBox()
+ {
+ InitializeComponent();
+ }
+
+ #region Dependency Properties
+
+ public static readonly DependencyProperty WatermarkProperty =
+ DependencyProperty.Register(nameof(Watermark), typeof(string), typeof(WatermarkTextBox),
+ new PropertyMetadata(string.Empty));
+
+ public string Watermark
+ {
+ get => (string)GetValue(WatermarkProperty);
+ set => SetValue(WatermarkProperty, value);
+ }
+
+ public static readonly DependencyProperty TextProperty =
+ DependencyProperty.Register(nameof(Text), typeof(string), typeof(WatermarkTextBox),
+ new FrameworkPropertyMetadata(string.Empty, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
+
+ public string Text
+ {
+ get => (string)GetValue(TextProperty);
+ set => SetValue(TextProperty, value);
+ }
+
+ public static readonly DependencyProperty VerticalContentAlignmentProperty =
+ DependencyProperty.Register(nameof(VerticalContentAlignment), typeof(VerticalAlignment), typeof(WatermarkTextBox),
+ new PropertyMetadata(VerticalAlignment.Center));
+
+ public new VerticalAlignment VerticalContentAlignment
+ {
+ get => (VerticalAlignment)GetValue(VerticalContentAlignmentProperty);
+ set => SetValue(VerticalContentAlignmentProperty, value);
+ }
+
+ public static readonly DependencyProperty IsReadOnlyProperty =
+ DependencyProperty.Register(nameof(IsReadOnly), typeof(bool), typeof(WatermarkTextBox),
+ new PropertyMetadata(false));
+
+ public bool IsReadOnly
+ {
+ get => (bool)GetValue(IsReadOnlyProperty);
+ set => SetValue(IsReadOnlyProperty, value);
+ }
+
+ #endregion
+
+ #region Events
+
+ public event TextChangedEventHandler TextChanged
+ {
+ add => PART_TextBox.TextChanged += value;
+ remove => PART_TextBox.TextChanged -= value;
+ }
+
+ public new event KeyEventHandler KeyUp
+ {
+ add => PART_TextBox.KeyUp += value;
+ remove => PART_TextBox.KeyUp -= value;
+ }
+
+ public new event KeyEventHandler KeyDown
+ {
+ add => PART_TextBox.KeyDown += value;
+ remove => PART_TextBox.KeyDown -= value;
+ }
+
+ #endregion
+
+ #region Methods
+
+ public new void Focus()
+ {
+ PART_TextBox.Focus();
+ }
+
+ public void SelectAll()
+ {
+ PART_TextBox.SelectAll();
+ }
+
+ public void Clear()
+ {
+ PART_TextBox.Clear();
+ Text = string.Empty;
+ }
+
+ #endregion
+ }
+}
diff --git a/LegendaryExplorer/LegendaryExplorer/SharedUI/Converters/PackageEditorWPFConverters.cs b/LegendaryExplorer/LegendaryExplorer/SharedUI/Converters/PackageEditorWPFConverters.cs
index 46bed895cc..3925beaf4f 100644
--- a/LegendaryExplorer/LegendaryExplorer/SharedUI/Converters/PackageEditorWPFConverters.cs
+++ b/LegendaryExplorer/LegendaryExplorer/SharedUI/Converters/PackageEditorWPFConverters.cs
@@ -28,7 +28,8 @@ public object Convert(object value, Type targetType, object parameter, CultureIn
}
if (value.ToString() == (string)parameter)
{
- return Brushes.LightBlue;
+ // Use system highlight color for theme support
+ return (SolidColorBrush)Application.Current.FindResource(SystemColors.HighlightBrushKey);
}
return Brushes.Transparent;
}
diff --git a/LegendaryExplorer/LegendaryExplorer/SharedUI/Converters/UnsavedChangesForegroundConverter.cs b/LegendaryExplorer/LegendaryExplorer/SharedUI/Converters/UnsavedChangesForegroundConverter.cs
index 85d4ce76f3..73e2e5c3e9 100644
--- a/LegendaryExplorer/LegendaryExplorer/SharedUI/Converters/UnsavedChangesForegroundConverter.cs
+++ b/LegendaryExplorer/LegendaryExplorer/SharedUI/Converters/UnsavedChangesForegroundConverter.cs
@@ -1,5 +1,6 @@
using System;
using System.Globalization;
+using System.Windows;
using System.Windows.Data;
using System.Windows.Media;
@@ -15,7 +16,8 @@ public class UnsavedChangesForegroundConverter : IValueConverter
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if ((bool)value) { return new SolidColorBrush(Colors.Red); }
- return new SolidColorBrush(Colors.Black);
+ // Use system color for normal state to respect dark/light themes
+ return SystemColors.ControlTextBrush;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
diff --git a/LegendaryExplorer/LegendaryExplorer/SharedUI/TreeViewEntry.cs b/LegendaryExplorer/LegendaryExplorer/SharedUI/TreeViewEntry.cs
index 81ac32f06e..d453827079 100644
--- a/LegendaryExplorer/LegendaryExplorer/SharedUI/TreeViewEntry.cs
+++ b/LegendaryExplorer/LegendaryExplorer/SharedUI/TreeViewEntry.cs
@@ -534,19 +534,10 @@ private bool AddPropertyFlags(ExportEntry ee)
public int UIndex => Entry?.UIndex ?? 0;
- private System.Windows.Media.Brush _foregroundColor;
- public System.Windows.Media.Brush ForegroundColor
- {
- get => Entry is ImportEntry ? ImportEntryBrush : ExportEntryBrush;
- set
- {
- _foregroundColor = value;
- OnPropertyChanged();
- }
- }
-
- private static SolidColorBrush ImportEntryBrush => SystemColors.GrayTextBrush;
- private static SolidColorBrush ExportEntryBrush => SystemColors.ControlTextBrush;
+ ///
+ /// Returns true if this entry is an ImportEntry, used for XAML DataTrigger binding
+ ///
+ public bool IsImport => Entry is ImportEntry;
public override string ToString()
{
diff --git a/LegendaryExplorer/LegendaryExplorer/Startup/AppBoot.cs b/LegendaryExplorer/LegendaryExplorer/Startup/AppBoot.cs
index 699966a4d8..af875a2eee 100644
--- a/LegendaryExplorer/LegendaryExplorer/Startup/AppBoot.cs
+++ b/LegendaryExplorer/LegendaryExplorer/Startup/AppBoot.cs
@@ -22,6 +22,7 @@
using LegendaryExplorerCore.Misc;
using LegendaryExplorerCore.Packages;
using Serilog;
+using MessageBox = Xceed.Wpf.Toolkit.MessageBox;
namespace LegendaryExplorer.Startup
{
@@ -86,6 +87,9 @@ public static void Startup(App app)
}
Settings.LoadSettings();
+
+ // Apply theme based on settings
+ ThemeManager.ApplyTheme();
ToolSet.Initialize();
app.ShutdownMode = ShutdownMode.OnExplicitShutdown;
diff --git a/LegendaryExplorer/LegendaryExplorer/Startup/DependencyCheck.cs b/LegendaryExplorer/LegendaryExplorer/Startup/DependencyCheck.cs
index a9f92e407f..17d4a49455 100644
--- a/LegendaryExplorer/LegendaryExplorer/Startup/DependencyCheck.cs
+++ b/LegendaryExplorer/LegendaryExplorer/Startup/DependencyCheck.cs
@@ -8,6 +8,7 @@
using LegendaryExplorer.Misc;
using Microsoft.AppCenter.Analytics;
using Microsoft.Win32;
+using MessageBox = Xceed.Wpf.Toolkit.MessageBox;
namespace LegendaryExplorer.Startup
{
diff --git a/LegendaryExplorer/LegendaryExplorer/Tools/AFCCompactor/AFCCompactorWindow.xaml b/LegendaryExplorer/LegendaryExplorer/Tools/AFCCompactor/AFCCompactorWindow.xaml
index 0a9a192ebb..81d437f655 100644
--- a/LegendaryExplorer/LegendaryExplorer/Tools/AFCCompactor/AFCCompactorWindow.xaml
+++ b/LegendaryExplorer/LegendaryExplorer/Tools/AFCCompactor/AFCCompactorWindow.xaml
@@ -1,4 +1,4 @@
-
-
diff --git a/LegendaryExplorer/LegendaryExplorer/Tools/AFCCompactor/AFCCompactorWindow.xaml.cs b/LegendaryExplorer/LegendaryExplorer/Tools/AFCCompactor/AFCCompactorWindow.xaml.cs
index f6443a8a71..a713769670 100644
--- a/LegendaryExplorer/LegendaryExplorer/Tools/AFCCompactor/AFCCompactorWindow.xaml.cs
+++ b/LegendaryExplorer/LegendaryExplorer/Tools/AFCCompactor/AFCCompactorWindow.xaml.cs
@@ -21,6 +21,7 @@
using Microsoft.WindowsAPICodePack.Dialogs;
using AFCCompactor = LegendaryExplorerCore.Audio.AFCCompactor;
using Application = System.Windows.Application;
+using MessageBox = Xceed.Wpf.Toolkit.MessageBox;
namespace LegendaryExplorer.Tools.AFCCompactorWindow
{
diff --git a/LegendaryExplorer/LegendaryExplorer/Tools/AnimationImporterExporter/AnimationImporterExporterWindow.xaml b/LegendaryExplorer/LegendaryExplorer/Tools/AnimationImporterExporter/AnimationImporterExporterWindow.xaml
index 7830d230bf..bf0ece14d5 100644
--- a/LegendaryExplorer/LegendaryExplorer/Tools/AnimationImporterExporter/AnimationImporterExporterWindow.xaml
+++ b/LegendaryExplorer/LegendaryExplorer/Tools/AnimationImporterExporter/AnimationImporterExporterWindow.xaml
@@ -1,4 +1,4 @@
-
Animation Importer is still relatively new, if you have issues, please report them to one of the following places:
- -
- The issues list on GitHub at
+ Foreground="{DynamicResource {x:Static SystemColors.HotTrackBrushKey}}" misc:HyperlinkExtensions.IsExternal="True">
@@ -134,7 +134,7 @@
-
+
diff --git a/LegendaryExplorer/LegendaryExplorer/Tools/AnimationImporterExporter/AnimationImporterExporterWindow.xaml.cs b/LegendaryExplorer/LegendaryExplorer/Tools/AnimationImporterExporter/AnimationImporterExporterWindow.xaml.cs
index 0df4b071fd..fb606d519f 100644
--- a/LegendaryExplorer/LegendaryExplorer/Tools/AnimationImporterExporter/AnimationImporterExporterWindow.xaml.cs
+++ b/LegendaryExplorer/LegendaryExplorer/Tools/AnimationImporterExporter/AnimationImporterExporterWindow.xaml.cs
@@ -21,6 +21,7 @@
using Microsoft.AppCenter.Analytics;
using Microsoft.Win32;
using Path = System.IO.Path;
+using MessageBox = Xceed.Wpf.Toolkit.MessageBox;
namespace LegendaryExplorer.Tools.AnimationImporterExporter
{
diff --git a/LegendaryExplorer/LegendaryExplorer/Tools/AnimationViewer/AnimationViewerWindow.xaml b/LegendaryExplorer/LegendaryExplorer/Tools/AnimationViewer/AnimationViewerWindow.xaml
index fc5ea26c59..77819b7292 100644
--- a/LegendaryExplorer/LegendaryExplorer/Tools/AnimationViewer/AnimationViewerWindow.xaml
+++ b/LegendaryExplorer/LegendaryExplorer/Tools/AnimationViewer/AnimationViewerWindow.xaml
@@ -1,4 +1,4 @@
-
-
+
-
+
-
+
@@ -134,7 +134,7 @@
-
+
@@ -144,7 +144,7 @@
-
+
@@ -159,7 +159,7 @@
+
+
@@ -44,6 +52,12 @@
@@ -70,9 +84,9 @@
-
-
-
+
+
+
-
+
@@ -455,14 +469,14 @@
-
+
-
+
@@ -50,4 +50,4 @@
-
\ No newline at end of file
+
diff --git a/LegendaryExplorer/LegendaryExplorer/Tools/CoalescedCompiler/CoalescedCompilerWindow.xaml.cs b/LegendaryExplorer/LegendaryExplorer/Tools/CoalescedCompiler/CoalescedCompilerWindow.xaml.cs
index 682bba5f56..4f2ec299e3 100644
--- a/LegendaryExplorer/LegendaryExplorer/Tools/CoalescedCompiler/CoalescedCompilerWindow.xaml.cs
+++ b/LegendaryExplorer/LegendaryExplorer/Tools/CoalescedCompiler/CoalescedCompilerWindow.xaml.cs
@@ -10,6 +10,7 @@
using LegendaryExplorerCore.Coalesced;
using LegendaryExplorerCore.Unreal;
using Microsoft.WindowsAPICodePack.Dialogs;
+using MessageBox = Xceed.Wpf.Toolkit.MessageBox;
namespace LegendaryExplorer.Tools.CoalescedCompiler
{
diff --git a/LegendaryExplorer/LegendaryExplorer/Tools/ConditionalsEditor/ConditionalsEditorWindow.xaml b/LegendaryExplorer/LegendaryExplorer/Tools/ConditionalsEditor/ConditionalsEditorWindow.xaml
index 0d4e4f0ad7..3ccee05f32 100644
--- a/LegendaryExplorer/LegendaryExplorer/Tools/ConditionalsEditor/ConditionalsEditorWindow.xaml
+++ b/LegendaryExplorer/LegendaryExplorer/Tools/ConditionalsEditor/ConditionalsEditorWindow.xaml
@@ -1,4 +1,4 @@
-
-
diff --git a/LegendaryExplorer/LegendaryExplorer/Tools/ConditionalsEditor/ConditionalsEditorWindow.xaml.cs b/LegendaryExplorer/LegendaryExplorer/Tools/ConditionalsEditor/ConditionalsEditorWindow.xaml.cs
index 15f72a1233..3afc570822 100644
--- a/LegendaryExplorer/LegendaryExplorer/Tools/ConditionalsEditor/ConditionalsEditorWindow.xaml.cs
+++ b/LegendaryExplorer/LegendaryExplorer/Tools/ConditionalsEditor/ConditionalsEditorWindow.xaml.cs
@@ -25,6 +25,11 @@
using LegendaryExplorerCore.PlotDatabase;
using LegendaryExplorerCore.Unreal;
using Microsoft.Win32;
+using Xceed.Wpf.Toolkit;
+using MessageBox = Xceed.Wpf.Toolkit.MessageBox;
+using MessageBoxResult = System.Windows.MessageBoxResult;
+using MessageBoxButton = System.Windows.MessageBoxButton;
+using MessageBoxImage = System.Windows.MessageBoxImage;
namespace LegendaryExplorer.Tools.ConditionalsEditor
{
@@ -129,6 +134,9 @@ private void ConditionalsEditorWindow_OnLoaded(object sender, RoutedEventArgs e)
hexBox.ByteProvider = new ReadOptimizedByteProvider();
this.bind(HexBoxMinWidthProperty, hexBox, nameof(hexBox.MinWidth));
this.bind(HexBoxMaxWidthProperty, hexBox, nameof(hexBox.MaxWidth));
+
+ // Register HexBox for theme management
+ Misc.ThemeManager.RegisterHexBox(hexBox);
}
public ICommand OpenCommand { get; set; }
diff --git a/LegendaryExplorer/LegendaryExplorer/Tools/CustomFilesManager/CustomFilesManagerWindow.xaml b/LegendaryExplorer/LegendaryExplorer/Tools/CustomFilesManager/CustomFilesManagerWindow.xaml
index a91eae1da7..1180f82301 100644
--- a/LegendaryExplorer/LegendaryExplorer/Tools/CustomFilesManager/CustomFilesManagerWindow.xaml
+++ b/LegendaryExplorer/LegendaryExplorer/Tools/CustomFilesManager/CustomFilesManagerWindow.xaml
@@ -1,4 +1,4 @@
-
-
+
diff --git a/LegendaryExplorer/LegendaryExplorer/Tools/Dialogue Editor/BulkInterpEditorDialog.xaml b/LegendaryExplorer/LegendaryExplorer/Tools/Dialogue Editor/BulkInterpEditorDialog.xaml
new file mode 100644
index 0000000000..2172b081d8
--- /dev/null
+++ b/LegendaryExplorer/LegendaryExplorer/Tools/Dialogue Editor/BulkInterpEditorDialog.xaml
@@ -0,0 +1,91 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/LegendaryExplorer/LegendaryExplorer/Tools/Dialogue Editor/BulkInterpEditorDialog.xaml.cs b/LegendaryExplorer/LegendaryExplorer/Tools/Dialogue Editor/BulkInterpEditorDialog.xaml.cs
new file mode 100644
index 0000000000..6310abb19f
--- /dev/null
+++ b/LegendaryExplorer/LegendaryExplorer/Tools/Dialogue Editor/BulkInterpEditorDialog.xaml.cs
@@ -0,0 +1,398 @@
+using LegendaryExplorerCore.Dialogue;
+using LegendaryExplorerCore.Kismet;
+using LegendaryExplorerCore.Misc;
+using LegendaryExplorerCore.Packages;
+using LegendaryExplorerCore.Unreal;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Windows;
+using MessageBox = Xceed.Wpf.Toolkit.MessageBox;
+
+namespace LegendaryExplorer.DialogueEditor
+{
+ ///
+ /// A data item representing an InterpGroup or Track and its editable properties.
+ ///
+ public class InterpGroupItem : INotifyPropertyChanged
+ {
+ ///
+ /// The type of item this represents.
+ ///
+ public enum ItemType
+ {
+ InterpGroup,
+ Track
+ }
+
+ public ItemType Type { get; set; }
+ public ExportEntry Export { get; set; }
+ public ExportEntry SeqActInterp { get; set; }
+ public ExportEntry ParentInterpGroup { get; set; }
+
+ public string ExportName => Export?.ObjectName.Instanced ?? "Unknown";
+ public string ExportClass => Export?.ClassName ?? "";
+
+ private string _groupName;
+ public string GroupName
+ {
+ get => _groupName;
+ set
+ {
+ if (_groupName != value)
+ {
+ _groupName = value;
+ IsModified = true;
+ OnPropertyChanged();
+ OnPropertyChanged(nameof(IsModified));
+ }
+ }
+ }
+
+ private string _originalGroupName;
+ public string OriginalGroupName
+ {
+ get => _originalGroupName;
+ set => _originalGroupName = value;
+ }
+
+ private string _sfxFindActor;
+ public string SFXFindActor
+ {
+ get => _sfxFindActor;
+ set
+ {
+ if (_sfxFindActor != value)
+ {
+ _sfxFindActor = value;
+ IsModified = true;
+ OnPropertyChanged();
+ OnPropertyChanged(nameof(IsModified));
+ }
+ }
+ }
+
+ private string _originalSFXFindActor;
+ public string OriginalSFXFindActor
+ {
+ get => _originalSFXFindActor;
+ set => _originalSFXFindActor = value;
+ }
+
+ private string _trackFindActor;
+ public string TrackFindActor
+ {
+ get => _trackFindActor;
+ set
+ {
+ if (_trackFindActor != value)
+ {
+ _trackFindActor = value;
+ IsModified = true;
+ OnPropertyChanged();
+ OnPropertyChanged(nameof(IsModified));
+ }
+ }
+ }
+
+ private string _originalTrackFindActor;
+ public string OriginalTrackFindActor
+ {
+ get => _originalTrackFindActor;
+ set => _originalTrackFindActor = value;
+ }
+
+ public bool IsModified { get; set; }
+
+ public event PropertyChangedEventHandler PropertyChanged;
+ protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+ }
+
+ ///
+ /// Dialog for bulk editing InterpGroup properties (GroupName, m_nmSFXFindActor, m_nmFindActor)
+ /// in a dialogue node's InterpData.
+ ///
+ public partial class BulkInterpEditorDialog : Window
+ {
+ public ObservableCollectionExtended InterpGroupItems { get; } = new();
+
+ private readonly DialogueNodeExtended _dialogueNode;
+ private readonly ConversationExtended _conversation;
+ private readonly IMEPackage _pcc;
+
+ public BulkInterpEditorDialog(Window owner, DialogueNodeExtended dialogueNode, ConversationExtended conversation)
+ {
+ _dialogueNode = dialogueNode;
+ _conversation = conversation;
+ _pcc = conversation.Export.FileRef;
+
+ InitializeComponent();
+ Owner = owner;
+
+ LoadInterpGroups();
+ }
+
+ ///
+ /// Loads all InterpGroups and tracks from the dialogue node's InterpData.
+ ///
+ private void LoadInterpGroups()
+ {
+ InterpGroupItems.ClearEx();
+
+ if (_dialogueNode?.InterpData == null)
+ {
+ return;
+ }
+
+ ExportEntry interpData = _dialogueNode.InterpData;
+ ExportEntry seqActInterp = FindSeqActInterp(interpData);
+
+ // Get the InterpGroups from the InterpData
+ var interpGroups = interpData.GetProperty>("InterpGroups");
+ if (interpGroups == null) return;
+
+ foreach (var groupRef in interpGroups)
+ {
+ if (!_pcc.TryGetUExport(groupRef.Value, out ExportEntry interpGroup))
+ continue;
+
+ // Add the InterpGroup item
+ var groupItem = new InterpGroupItem
+ {
+ Type = InterpGroupItem.ItemType.InterpGroup,
+ Export = interpGroup,
+ SeqActInterp = seqActInterp
+ };
+
+ // Get GroupName
+ var groupNameProp = interpGroup.GetProperty("GroupName");
+ groupItem.GroupName = groupNameProp?.Value.Instanced ?? "";
+ groupItem.OriginalGroupName = groupItem.GroupName;
+
+ // Get m_nmSFXFindActor (Game 3 specific)
+ var sfxFindActorProp = interpGroup.GetProperty("m_nmSFXFindActor");
+ groupItem.SFXFindActor = sfxFindActorProp?.Value.Instanced ?? "";
+ groupItem.OriginalSFXFindActor = groupItem.SFXFindActor;
+
+ // Reset the modified flag after initial load
+ groupItem.IsModified = false;
+ InterpGroupItems.Add(groupItem);
+
+ // Now look for ALL tracks with m_nmFindActor
+ var interpTracks = interpGroup.GetProperty>("InterpTracks");
+ if (interpTracks != null)
+ {
+ foreach (var trackRef in interpTracks)
+ {
+ if (!_pcc.TryGetUExport(trackRef.Value, out ExportEntry track))
+ continue;
+
+ var findActorProp = track.GetProperty("m_nmFindActor");
+ if (findActorProp != null)
+ {
+ var trackItem = new InterpGroupItem
+ {
+ Type = InterpGroupItem.ItemType.Track,
+ Export = track,
+ ParentInterpGroup = interpGroup,
+ SeqActInterp = seqActInterp,
+ TrackFindActor = findActorProp.Value.Instanced,
+ OriginalTrackFindActor = findActorProp.Value.Instanced,
+ IsModified = false
+ };
+ InterpGroupItems.Add(trackItem);
+ }
+ }
+ }
+ }
+ }
+
+ ///
+ /// Finds the SeqAct_Interp that references the given InterpData.
+ ///
+ private ExportEntry FindSeqActInterp(ExportEntry interpData)
+ {
+ var refs = interpData.GetEntriesThatReferenceThisOne();
+ foreach (var entry in refs.Keys)
+ {
+ if (entry.ClassName == "SeqAct_Interp")
+ {
+ return entry as ExportEntry;
+ }
+ }
+ return null;
+ }
+
+ ///
+ /// Applies bulk find/replace to the selected properties.
+ ///
+ private void BulkReplace_Click(object sender, RoutedEventArgs e)
+ {
+ string findText = FindTextBox.Text;
+ string replaceText = ReplaceTextBox.Text;
+
+ if (string.IsNullOrEmpty(findText))
+ {
+ MessageBox.Show("Please enter text to find.", "Warning", MessageBoxButton.OK, MessageBoxImage.Warning);
+ return;
+ }
+
+ int replacements = 0;
+
+ foreach (var item in InterpGroupItems)
+ {
+ if (item.Type == InterpGroupItem.ItemType.InterpGroup)
+ {
+ if (ReplaceGroupName.IsChecked == true && !string.IsNullOrEmpty(item.GroupName))
+ {
+ string newValue = item.GroupName.Replace(findText, replaceText);
+ if (newValue != item.GroupName)
+ {
+ item.GroupName = newValue;
+ replacements++;
+ }
+ }
+
+ if (ReplaceSFXFindActor.IsChecked == true && !string.IsNullOrEmpty(item.SFXFindActor))
+ {
+ string newValue = item.SFXFindActor.Replace(findText, replaceText);
+ if (newValue != item.SFXFindActor)
+ {
+ item.SFXFindActor = newValue;
+ replacements++;
+ }
+ }
+ }
+ else if (item.Type == InterpGroupItem.ItemType.Track)
+ {
+ if (ReplaceTrackFindActor.IsChecked == true && !string.IsNullOrEmpty(item.TrackFindActor))
+ {
+ string newValue = item.TrackFindActor.Replace(findText, replaceText);
+ if (newValue != item.TrackFindActor)
+ {
+ item.TrackFindActor = newValue;
+ replacements++;
+ }
+ }
+ }
+ }
+
+ MessageBox.Show($"Made {replacements} replacement(s).", "Bulk Replace", MessageBoxButton.OK, MessageBoxImage.Information);
+ }
+
+ ///
+ /// Applies all changes to the package.
+ ///
+ private void Apply_Click(object sender, RoutedEventArgs e)
+ {
+ int changesApplied = 0;
+
+ foreach (var item in InterpGroupItems.Where(i => i.IsModified))
+ {
+ if (item.Type == InterpGroupItem.ItemType.InterpGroup)
+ {
+ var groupProps = item.Export.GetProperties();
+
+ // Update GroupName
+ if (item.GroupName != item.OriginalGroupName)
+ {
+ if (!string.IsNullOrEmpty(item.GroupName))
+ {
+ groupProps.AddOrReplaceProp(new NameProperty(item.GroupName, "GroupName"));
+ }
+ else
+ {
+ groupProps.RemoveNamedProperty("GroupName");
+ }
+
+ // Also update the SeqAct_Interp VariableLinks if GroupName changed
+ UpdateSeqActInterpVariableLink(item);
+
+ changesApplied++;
+ }
+
+ // Update m_nmSFXFindActor
+ if (item.SFXFindActor != item.OriginalSFXFindActor)
+ {
+ if (!string.IsNullOrEmpty(item.SFXFindActor))
+ {
+ groupProps.AddOrReplaceProp(new NameProperty(item.SFXFindActor, "m_nmSFXFindActor"));
+ }
+ else
+ {
+ groupProps.RemoveNamedProperty("m_nmSFXFindActor");
+ }
+ changesApplied++;
+ }
+
+ item.Export.WriteProperties(groupProps);
+ }
+ else if (item.Type == InterpGroupItem.ItemType.Track)
+ {
+ // Update m_nmFindActor on the track
+ if (item.TrackFindActor != item.OriginalTrackFindActor)
+ {
+ var trackProps = item.Export.GetProperties();
+ if (!string.IsNullOrEmpty(item.TrackFindActor))
+ {
+ trackProps.AddOrReplaceProp(new NameProperty(item.TrackFindActor, "m_nmFindActor"));
+ }
+ else
+ {
+ trackProps.RemoveNamedProperty("m_nmFindActor");
+ }
+ item.Export.WriteProperties(trackProps);
+ changesApplied++;
+ }
+ }
+ }
+
+ if (changesApplied > 0)
+ {
+ MessageBox.Show($"Applied {changesApplied} change(s).", "Success", MessageBoxButton.OK, MessageBoxImage.Information);
+ }
+
+ DialogResult = true;
+ Close();
+ }
+
+ ///
+ /// Updates the SeqAct_Interp's VariableLinks to match the new group name.
+ ///
+ private void UpdateSeqActInterpVariableLink(InterpGroupItem item)
+ {
+ if (item.SeqActInterp == null || string.IsNullOrEmpty(item.OriginalGroupName))
+ return;
+
+ var varLinksProp = item.SeqActInterp.GetProperty>("VariableLinks");
+ if (varLinksProp == null) return;
+
+ bool modified = false;
+ foreach (var varLink in varLinksProp)
+ {
+ var linkDesc = varLink.GetProp("LinkDesc");
+ if (linkDesc != null && linkDesc.Value == item.OriginalGroupName)
+ {
+ linkDesc.Value = item.GroupName;
+ modified = true;
+ }
+ }
+
+ if (modified)
+ {
+ item.SeqActInterp.WriteProperty(varLinksProp);
+ }
+ }
+
+ private void Cancel_Click(object sender, RoutedEventArgs e)
+ {
+ DialogResult = false;
+ Close();
+ }
+ }
+}
diff --git a/LegendaryExplorer/LegendaryExplorer/Tools/Dialogue Editor/DialogueEditorExperiments/DialogueEditorExperimentsE.cs b/LegendaryExplorer/LegendaryExplorer/Tools/Dialogue Editor/DialogueEditorExperiments/DialogueEditorExperimentsE.cs
index c38e969e57..4f493b0d31 100644
--- a/LegendaryExplorer/LegendaryExplorer/Tools/Dialogue Editor/DialogueEditorExperiments/DialogueEditorExperimentsE.cs
+++ b/LegendaryExplorer/LegendaryExplorer/Tools/Dialogue Editor/DialogueEditorExperiments/DialogueEditorExperimentsE.cs
@@ -18,6 +18,7 @@
using static LegendaryExplorer.Misc.ExperimentsTools.DialogueAutomations;
using static LegendaryExplorer.Misc.ExperimentsTools.SequenceAutomations;
using LegendaryExplorerCore.Gammtek.Extensions.Collections.Generic;
+using MessageBox = Xceed.Wpf.Toolkit.MessageBox;
namespace LegendaryExplorer.DialogueEditor.DialogueEditorExperiments
{
diff --git a/LegendaryExplorer/LegendaryExplorer/Tools/Dialogue Editor/DialogueEditorExperiments/DialogueEditorExperimentsM.cs b/LegendaryExplorer/LegendaryExplorer/Tools/Dialogue Editor/DialogueEditorExperiments/DialogueEditorExperimentsM.cs
index 8ea86c399d..3871b430d9 100644
--- a/LegendaryExplorer/LegendaryExplorer/Tools/Dialogue Editor/DialogueEditorExperiments/DialogueEditorExperimentsM.cs
+++ b/LegendaryExplorer/LegendaryExplorer/Tools/Dialogue Editor/DialogueEditorExperiments/DialogueEditorExperimentsM.cs
@@ -14,6 +14,7 @@
using System.Text;
using System.Threading.Tasks;
using System.Windows;
+using MessageBox = Xceed.Wpf.Toolkit.MessageBox;
namespace LegendaryExplorer.Tools.Dialogue_Editor.DialogueEditorExperiments
{
diff --git a/LegendaryExplorer/LegendaryExplorer/Tools/Dialogue Editor/DialogueEditorExperiments/DialogueExperimentsMenuControl.xaml b/LegendaryExplorer/LegendaryExplorer/Tools/Dialogue Editor/DialogueEditorExperiments/DialogueExperimentsMenuControl.xaml
index b0a69a3b44..943c9903bd 100644
--- a/LegendaryExplorer/LegendaryExplorer/Tools/Dialogue Editor/DialogueEditorExperiments/DialogueExperimentsMenuControl.xaml
+++ b/LegendaryExplorer/LegendaryExplorer/Tools/Dialogue Editor/DialogueEditorExperiments/DialogueExperimentsMenuControl.xaml
@@ -1,13 +1,14 @@
-
@@ -872,6 +893,8 @@
HorizontalAlignment="Stretch" FontSize="15"
VerticalAlignment="Center" HorizontalContentAlignment="Left"
BorderBrush="LightGray" BorderThickness="0"
+ Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}"
+ Foreground="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"
GotKeyboardFocus="EditBox_GotKeyboardFocus"
LostKeyboardFocus="EditBox_LostKeyboardFocus"
KeyUp="EditBox_Node_KeyUp"
@@ -898,7 +921,9 @@
+ Margin="2"
+ Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}"
+ Foreground="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}" />
-
-
+
+
-
\ No newline at end of file
+
diff --git a/LegendaryExplorer/LegendaryExplorer/Tools/InterpEditor/InterpEditorWindow.xaml.cs b/LegendaryExplorer/LegendaryExplorer/Tools/InterpEditor/InterpEditorWindow.xaml.cs
index 2231582c63..3862861d82 100644
--- a/LegendaryExplorer/LegendaryExplorer/Tools/InterpEditor/InterpEditorWindow.xaml.cs
+++ b/LegendaryExplorer/LegendaryExplorer/Tools/InterpEditor/InterpEditorWindow.xaml.cs
@@ -17,6 +17,7 @@
using LegendaryExplorerCore.Packages;
using Microsoft.Win32;
using Path = System.IO.Path;
+using MessageBox = Xceed.Wpf.Toolkit.MessageBox;
namespace LegendaryExplorer.Tools.InterpEditor
{
diff --git a/LegendaryExplorer/LegendaryExplorer/Tools/InterpEditor/InterpExperiments/InterpEditorExperimentsE.cs b/LegendaryExplorer/LegendaryExplorer/Tools/InterpEditor/InterpExperiments/InterpEditorExperimentsE.cs
index 3847824b9b..164c60eb71 100644
--- a/LegendaryExplorer/LegendaryExplorer/Tools/InterpEditor/InterpExperiments/InterpEditorExperimentsE.cs
+++ b/LegendaryExplorer/LegendaryExplorer/Tools/InterpEditor/InterpExperiments/InterpEditorExperimentsE.cs
@@ -8,6 +8,7 @@
using System.Linq;
using System.Windows;
using static LegendaryExplorer.Misc.ExperimentsTools.SharedMethods;
+using MessageBox = Xceed.Wpf.Toolkit.MessageBox;
namespace LegendaryExplorer.Tools.InterpEditor.InterpExperiments
{
diff --git a/LegendaryExplorer/LegendaryExplorer/Tools/InterpEditor/InterpExperiments/InterpEditorExperimentsH.cs b/LegendaryExplorer/LegendaryExplorer/Tools/InterpEditor/InterpExperiments/InterpEditorExperimentsH.cs
index 23d90069a1..32ddf60e62 100644
--- a/LegendaryExplorer/LegendaryExplorer/Tools/InterpEditor/InterpExperiments/InterpEditorExperimentsH.cs
+++ b/LegendaryExplorer/LegendaryExplorer/Tools/InterpEditor/InterpExperiments/InterpEditorExperimentsH.cs
@@ -10,6 +10,7 @@
using LegendaryExplorerCore.Packages.CloningImportingAndRelinking;
using LegendaryExplorerCore.Unreal;
using LibVLCSharp.Shared;
+using MessageBox = Xceed.Wpf.Toolkit.MessageBox;
namespace LegendaryExplorer.Tools.InterpEditor.InterpExperiments
{
diff --git a/LegendaryExplorer/LegendaryExplorer/Tools/InterpEditor/InterpExperiments/InterpExperimentsMenuControl.xaml b/LegendaryExplorer/LegendaryExplorer/Tools/InterpEditor/InterpExperiments/InterpExperimentsMenuControl.xaml
index ad87ade4e3..670c11e54d 100644
--- a/LegendaryExplorer/LegendaryExplorer/Tools/InterpEditor/InterpExperiments/InterpExperimentsMenuControl.xaml
+++ b/LegendaryExplorer/LegendaryExplorer/Tools/InterpEditor/InterpExperiments/InterpExperimentsMenuControl.xaml
@@ -1,13 +1,14 @@
-
+
diff --git a/LegendaryExplorer/LegendaryExplorer/Tools/LevelEditor/LevelEditor.xaml b/LegendaryExplorer/LegendaryExplorer/Tools/LevelEditor/LevelEditor.xaml
index 0cabba1266..9f8fa17f62 100644
--- a/LegendaryExplorer/LegendaryExplorer/Tools/LevelEditor/LevelEditor.xaml
+++ b/LegendaryExplorer/LegendaryExplorer/Tools/LevelEditor/LevelEditor.xaml
@@ -1,4 +1,4 @@
-
-
+
@@ -162,7 +162,7 @@
-
+
@@ -252,17 +252,17 @@
+ Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}">
This is a work in progress, and only displays some of the actors in a level.
If you have issues with Level Editor, please report them to one of the following places:
- -
+ -
- The issues list on GitHub at
-
+
diff --git a/LegendaryExplorer/LegendaryExplorer/Tools/LevelEditor/LevelEditor.xaml.cs b/LegendaryExplorer/LegendaryExplorer/Tools/LevelEditor/LevelEditor.xaml.cs
index db7b957dc6..4be33ff0f1 100644
--- a/LegendaryExplorer/LegendaryExplorer/Tools/LevelEditor/LevelEditor.xaml.cs
+++ b/LegendaryExplorer/LegendaryExplorer/Tools/LevelEditor/LevelEditor.xaml.cs
@@ -21,6 +21,7 @@
using System.Windows;
using System.Windows.Input;
using System.Windows.Threading;
+using MessageBox = Xceed.Wpf.Toolkit.MessageBox;
namespace LegendaryExplorer.Tools.LevelEditor;
diff --git a/LegendaryExplorer/LegendaryExplorer/Tools/LevelEditor/Scene3D/MeshRenderContext.cs b/LegendaryExplorer/LegendaryExplorer/Tools/LevelEditor/Scene3D/MeshRenderContext.cs
index 0599adfac2..da9e69dd72 100644
--- a/LegendaryExplorer/LegendaryExplorer/Tools/LevelEditor/Scene3D/MeshRenderContext.cs
+++ b/LegendaryExplorer/LegendaryExplorer/Tools/LevelEditor/Scene3D/MeshRenderContext.cs
@@ -21,6 +21,7 @@
using DW = SharpDX.DirectWrite;
using Texture2D = SharpDX.Direct3D11.Texture2D;
using LECTexture2D = LegendaryExplorerCore.Unreal.Classes.Texture2D;
+using MessageBox = Xceed.Wpf.Toolkit.MessageBox;
namespace LegendaryExplorer.Tools.LevelEditor.Scene3D;
diff --git a/LegendaryExplorer/LegendaryExplorer/Tools/LevelEditor/Scene3D/SceneControlOptionsControl.xaml b/LegendaryExplorer/LegendaryExplorer/Tools/LevelEditor/Scene3D/SceneControlOptionsControl.xaml
index 080cfd547c..24d513afd5 100644
--- a/LegendaryExplorer/LegendaryExplorer/Tools/LevelEditor/Scene3D/SceneControlOptionsControl.xaml
+++ b/LegendaryExplorer/LegendaryExplorer/Tools/LevelEditor/Scene3D/SceneControlOptionsControl.xaml
@@ -1,14 +1,14 @@
-
-
+
+
diff --git a/LegendaryExplorer/LegendaryExplorer/Tools/LiveLevelEditor/EditorPanels/LLEActorEditorPanel.xaml b/LegendaryExplorer/LegendaryExplorer/Tools/LiveLevelEditor/EditorPanels/LLEActorEditorPanel.xaml
index 89fa9a3e12..6ad5c4ba09 100644
--- a/LegendaryExplorer/LegendaryExplorer/Tools/LiveLevelEditor/EditorPanels/LLEActorEditorPanel.xaml
+++ b/LegendaryExplorer/LegendaryExplorer/Tools/LiveLevelEditor/EditorPanels/LLEActorEditorPanel.xaml
@@ -1,4 +1,4 @@
-
-
+
@@ -135,7 +135,7 @@
-
+
@@ -171,7 +171,7 @@
-
+
diff --git a/LegendaryExplorer/LegendaryExplorer/Tools/LiveLevelEditor/LELiveLevelEditorWindow.xaml.cs b/LegendaryExplorer/LegendaryExplorer/Tools/LiveLevelEditor/LELiveLevelEditorWindow.xaml.cs
index b02f5850d4..dece03aaa2 100644
--- a/LegendaryExplorer/LegendaryExplorer/Tools/LiveLevelEditor/LELiveLevelEditorWindow.xaml.cs
+++ b/LegendaryExplorer/LegendaryExplorer/Tools/LiveLevelEditor/LELiveLevelEditorWindow.xaml.cs
@@ -34,6 +34,7 @@
using Newtonsoft.Json;
using System.Threading.Tasks;
using Microsoft.Win32;
+using MessageBox = Xceed.Wpf.Toolkit.MessageBox;
namespace LegendaryExplorer.Tools.LiveLevelEditor
{
diff --git a/LegendaryExplorer/LegendaryExplorer/Tools/LiveLevelEditor/LiveLevelEditorWindow.xaml b/LegendaryExplorer/LegendaryExplorer/Tools/LiveLevelEditor/LiveLevelEditorWindow.xaml
index 9d4889aa52..dd6710550c 100644
--- a/LegendaryExplorer/LegendaryExplorer/Tools/LiveLevelEditor/LiveLevelEditorWindow.xaml
+++ b/LegendaryExplorer/LegendaryExplorer/Tools/LiveLevelEditor/LiveLevelEditorWindow.xaml
@@ -104,7 +104,7 @@
-
+
@@ -128,7 +128,7 @@
-
+
@@ -152,7 +152,7 @@
-
+
@@ -163,7 +163,7 @@
-
+
@@ -196,7 +196,7 @@
-
+
diff --git a/LegendaryExplorer/LegendaryExplorer/Tools/LiveLevelEditor/LiveLevelEditorWindow.xaml.cs b/LegendaryExplorer/LegendaryExplorer/Tools/LiveLevelEditor/LiveLevelEditorWindow.xaml.cs
index 5516c7c178..f8fb17de05 100644
--- a/LegendaryExplorer/LegendaryExplorer/Tools/LiveLevelEditor/LiveLevelEditorWindow.xaml.cs
+++ b/LegendaryExplorer/LegendaryExplorer/Tools/LiveLevelEditor/LiveLevelEditorWindow.xaml.cs
@@ -26,6 +26,7 @@
using InterpCurveVector = LegendaryExplorerCore.Unreal.BinaryConverters.InterpCurve;
using InterpCurveFloat = LegendaryExplorerCore.Unreal.BinaryConverters.InterpCurve;
using LegendaryExplorer.Tools.PackageEditor;
+using MessageBox = Xceed.Wpf.Toolkit.MessageBox;
namespace LegendaryExplorer.Tools.LiveLevelEditor
{
diff --git a/LegendaryExplorer/LegendaryExplorer/Tools/LiveLevelEditor/MatEd/MaterialEditorLLE.xaml b/LegendaryExplorer/LegendaryExplorer/Tools/LiveLevelEditor/MatEd/MaterialEditorLLE.xaml
index f87d954c5e..4dfea9f33c 100644
--- a/LegendaryExplorer/LegendaryExplorer/Tools/LiveLevelEditor/MatEd/MaterialEditorLLE.xaml
+++ b/LegendaryExplorer/LegendaryExplorer/Tools/LiveLevelEditor/MatEd/MaterialEditorLLE.xaml
@@ -1,4 +1,4 @@
-
If you have issues with Mesh Explorer, please report them to one of the following places:
- -
+ -
- The issues list on GitHub at
-
+
-
+
+ HorizontalContentAlignment="Stretch"
+ Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}">
@@ -143,7 +145,7 @@
-
+
diff --git a/LegendaryExplorer/LegendaryExplorer/Tools/MountEditor/MountEditorWindow.xaml b/LegendaryExplorer/LegendaryExplorer/Tools/MountEditor/MountEditorWindow.xaml
index 44ba89bb84..853c2b4c27 100644
--- a/LegendaryExplorer/LegendaryExplorer/Tools/MountEditor/MountEditorWindow.xaml
+++ b/LegendaryExplorer/LegendaryExplorer/Tools/MountEditor/MountEditorWindow.xaml
@@ -1,4 +1,4 @@
-
-
+
diff --git a/LegendaryExplorer/LegendaryExplorer/Tools/MountEditor/MountEditorWindow.xaml.cs b/LegendaryExplorer/LegendaryExplorer/Tools/MountEditor/MountEditorWindow.xaml.cs
index 7ec6473439..1330e926f9 100644
--- a/LegendaryExplorer/LegendaryExplorer/Tools/MountEditor/MountEditorWindow.xaml.cs
+++ b/LegendaryExplorer/LegendaryExplorer/Tools/MountEditor/MountEditorWindow.xaml.cs
@@ -15,6 +15,7 @@
using LegendaryExplorerCore.TLK;
using LegendaryExplorerCore.Unreal;
using Xceed.Wpf.Toolkit.Primitives;
+using MessageBox = Xceed.Wpf.Toolkit.MessageBox;
namespace LegendaryExplorer.Tools.MountEditor
{
diff --git a/LegendaryExplorer/LegendaryExplorer/Tools/ObjectInstanceViewer/ObjectInstanceDBViewerWindow.xaml b/LegendaryExplorer/LegendaryExplorer/Tools/ObjectInstanceViewer/ObjectInstanceDBViewerWindow.xaml
index 74b830fe57..c9f0ec256d 100644
--- a/LegendaryExplorer/LegendaryExplorer/Tools/ObjectInstanceViewer/ObjectInstanceDBViewerWindow.xaml
+++ b/LegendaryExplorer/LegendaryExplorer/Tools/ObjectInstanceViewer/ObjectInstanceDBViewerWindow.xaml
@@ -1,4 +1,4 @@
-
-
+
@@ -77,4 +77,4 @@
-
\ No newline at end of file
+
diff --git a/LegendaryExplorer/LegendaryExplorer/Tools/PackageEditor/Experiments/PackageEditorExperimentsH.cs b/LegendaryExplorer/LegendaryExplorer/Tools/PackageEditor/Experiments/PackageEditorExperimentsH.cs
index 04130c69f0..621733071e 100644
--- a/LegendaryExplorer/LegendaryExplorer/Tools/PackageEditor/Experiments/PackageEditorExperimentsH.cs
+++ b/LegendaryExplorer/LegendaryExplorer/Tools/PackageEditor/Experiments/PackageEditorExperimentsH.cs
@@ -24,6 +24,7 @@
using Newtonsoft.Json;
using BioMorphFace = LegendaryExplorerCore.Unreal.Classes.BioMorphFace;
using Microsoft.VisualBasic;
+using MessageBox = Xceed.Wpf.Toolkit.MessageBox;
namespace LegendaryExplorer.Tools.PackageEditor.Experiments
{
diff --git a/LegendaryExplorer/LegendaryExplorer/Tools/PackageEditor/Experiments/PackageEditorExperimentsK.cs b/LegendaryExplorer/LegendaryExplorer/Tools/PackageEditor/Experiments/PackageEditorExperimentsK.cs
index 1f165e5cef..2f1d43d9ef 100644
--- a/LegendaryExplorer/LegendaryExplorer/Tools/PackageEditor/Experiments/PackageEditorExperimentsK.cs
+++ b/LegendaryExplorer/LegendaryExplorer/Tools/PackageEditor/Experiments/PackageEditorExperimentsK.cs
@@ -29,6 +29,7 @@
using Newtonsoft.Json;
using static LegendaryExplorer.Tools.ScriptDebugger.DebuggerInterface;
using Path = System.IO.Path;
+using MessageBox = Xceed.Wpf.Toolkit.MessageBox;
namespace LegendaryExplorer.Tools.PackageEditor.Experiments
{
diff --git a/LegendaryExplorer/LegendaryExplorer/Tools/PackageEditor/Experiments/PackageEditorExperimentsM.cs b/LegendaryExplorer/LegendaryExplorer/Tools/PackageEditor/Experiments/PackageEditorExperimentsM.cs
index b90436e873..b985bd3ed2 100644
--- a/LegendaryExplorer/LegendaryExplorer/Tools/PackageEditor/Experiments/PackageEditorExperimentsM.cs
+++ b/LegendaryExplorer/LegendaryExplorer/Tools/PackageEditor/Experiments/PackageEditorExperimentsM.cs
@@ -44,6 +44,7 @@
using LegendaryExplorerCore.UnrealScript.Documentation;
using LegendaryExplorer.SharedUI.Controls;
using LegendaryExplorerCore.Diagnostics;
+using MessageBox = Xceed.Wpf.Toolkit.MessageBox;
//using ImageMagick;
@@ -644,16 +645,29 @@ public static void RebuildFullLevelNetindexes()
}
}
- public static void ShiftInterpTrackMovesInPackage(IMEPackage package, Func predicate)
+ public static void ShiftInterpTrackMovesInPackage(IMEPackage package, Func predicate, ShiftInterpTrackParameters parameters = null)
{
- var offsetX = int.Parse(PromptDialog.Prompt(null, "Enter X shift offset", "Offset X", "0", true));
- var offsetY = int.Parse(PromptDialog.Prompt(null, "Enter Y shift offset", "Offset Y", "0", true));
- var offsetZ = int.Parse(PromptDialog.Prompt(null, "Enter Z shift offset", "Offset Z", "0", true));
- foreach (var exp in package.Exports.Where(x => x.ClassName == "InterpTrackMove"))
+ if (parameters == null)
{
- if (predicate == null || predicate.Invoke(exp))
+ var offsetX = int.Parse(PromptDialog.Prompt(null, "Enter X shift offset", "Offset X", "0", true));
+ var offsetY = int.Parse(PromptDialog.Prompt(null, "Enter Y shift offset", "Offset Y", "0", true));
+ var offsetZ = int.Parse(PromptDialog.Prompt(null, "Enter Z shift offset", "Offset Z", "0", true));
+ foreach (var exp in package.Exports.Where(x => x.ClassName == "InterpTrackMove"))
{
- ShiftInterpTrackMove(exp, offsetX, offsetY, offsetZ);
+ if (predicate == null || predicate.Invoke(exp))
+ {
+ ShiftInterpTrackMove(exp, offsetX, offsetY, offsetZ);
+ }
+ }
+ }
+ else
+ {
+ foreach (var exp in package.Exports.Where(x => x.ClassName == "InterpTrackMove"))
+ {
+ if (predicate == null || predicate.Invoke(exp))
+ {
+ ShiftInterpTrackMove(exp, parameters);
+ }
}
}
}
@@ -679,6 +693,93 @@ public static void ShiftInterpTrackMove(ExportEntry interpTrackMove, int? offset
interpTrackMove.WriteProperties(props);
}
+ public static void ShiftInterpTrackMove(ExportEntry interpTrackMove, ShiftInterpTrackParameters parameters)
+ {
+ var props = interpTrackMove.GetProperties();
+ var posTrack = props.GetProp("PosTrack");
+ var points = posTrack.GetProp>("Points");
+ var eulerTrack = props.GetProp("EulerTrack");
+ var eulerPoints = eulerTrack?.GetProp>("Points");
+ var lookupTrack = props.GetProp("LookupTrack");
+ var lookupPoints = lookupTrack?.GetProp>("Points");
+
+ for (int i = 0; i < points.Count; i++)
+ {
+ var point = points[i];
+ var outval = point.GetProp("OutVal");
+ outval.GetProp("X").Value += parameters.OffsetX;
+ outval.GetProp("Y").Value += parameters.OffsetY;
+ outval.GetProp("Z").Value += parameters.OffsetZ;
+
+ // Update time offset for position track
+ if (parameters.TimeOffset != 0)
+ {
+ var inVal = point.GetProp("InVal");
+ if (inVal != null)
+ {
+ inVal.Value += parameters.TimeOffset;
+ }
+ }
+ }
+
+ // Handle rotation (roll, pitch, yaw)
+ if ((parameters.Roll != 0 || parameters.Pitch != 0 || parameters.Yaw != 0) && eulerPoints != null)
+ {
+ for (int i = 0; i < eulerPoints.Count; i++)
+ {
+ var eulerPoint = eulerPoints[i];
+ var eulerVal = eulerPoint.GetProp("OutVal");
+ if (eulerVal != null)
+ {
+ var xProp = eulerVal.GetProp("X");
+ var yProp = eulerVal.GetProp("Y");
+ var zProp = eulerVal.GetProp("Z");
+
+ if (xProp != null) xProp.Value += parameters.Roll;
+ if (yProp != null) yProp.Value += parameters.Pitch;
+ if (zProp != null) zProp.Value += parameters.Yaw;
+ }
+ }
+ }
+
+ // Handle time offset for euler track (independent of rotation)
+ if (parameters.TimeOffset != 0 && eulerPoints != null)
+ {
+ for (int i = 0; i < eulerPoints.Count; i++)
+ {
+ var eulerPoint = eulerPoints[i];
+ var inVal = eulerPoint.GetProp("InVal");
+ if (inVal != null)
+ {
+ inVal.Value += parameters.TimeOffset;
+ }
+ }
+ }
+
+ // Handle time offset for lookup track
+ if (parameters.TimeOffset != 0 && lookupPoints != null)
+ {
+ for (int i = 0; i < lookupPoints.Count; i++)
+ {
+ var lookupPoint = lookupPoints[i];
+ var inVal = lookupPoint.GetProp("InVal");
+ if (inVal != null)
+ {
+ inVal.Value += parameters.TimeOffset;
+ }
+
+ // Update the Time property directly in the InterpLookupPoint structure
+ var timeProp = lookupPoint.GetProp("Time");
+ if (timeProp != null)
+ {
+ timeProp.Value += parameters.TimeOffset;
+ }
+ }
+ }
+
+ interpTrackMove.WriteProperties(props);
+ }
+
///
/// Shifts an ME1 AnimCutscene by specified X Y Z values. Only supports 96NoW (3 32-bit float) animations
/// By Mgamerz
diff --git a/LegendaryExplorer/LegendaryExplorer/Tools/PackageEditor/Experiments/PackageEditorExperimentsO.cs b/LegendaryExplorer/LegendaryExplorer/Tools/PackageEditor/Experiments/PackageEditorExperimentsO.cs
index aed27e4199..6e8f297677 100644
--- a/LegendaryExplorer/LegendaryExplorer/Tools/PackageEditor/Experiments/PackageEditorExperimentsO.cs
+++ b/LegendaryExplorer/LegendaryExplorer/Tools/PackageEditor/Experiments/PackageEditorExperimentsO.cs
@@ -23,6 +23,7 @@
using static LegendaryExplorer.Misc.ExperimentsTools.PackageAutomations;
using static LegendaryExplorer.Misc.ExperimentsTools.SequenceAutomations;
using static LegendaryExplorer.Misc.ExperimentsTools.SharedMethods;
+using MessageBox = Xceed.Wpf.Toolkit.MessageBox;
namespace LegendaryExplorer.Tools.PackageEditor.Experiments
{
diff --git a/LegendaryExplorer/LegendaryExplorer/Tools/PackageEditor/Experiments/PackageEditorExperimentsS.cs b/LegendaryExplorer/LegendaryExplorer/Tools/PackageEditor/Experiments/PackageEditorExperimentsS.cs
index 5f99041727..7aa9737f28 100644
--- a/LegendaryExplorer/LegendaryExplorer/Tools/PackageEditor/Experiments/PackageEditorExperimentsS.cs
+++ b/LegendaryExplorer/LegendaryExplorer/Tools/PackageEditor/Experiments/PackageEditorExperimentsS.cs
@@ -41,6 +41,7 @@
using TerraFX.Interop.Windows;
using static LegendaryExplorer.Tools.ScriptDebugger.DebuggerInterface;
using static LegendaryExplorerCore.Unreal.UnrealFlags;
+using MessageBox = Xceed.Wpf.Toolkit.MessageBox;
#pragma warning disable CS8321 //unused function warning
diff --git a/LegendaryExplorer/LegendaryExplorer/Tools/PackageEditor/Experiments/PackageEditorExperimentsSquid.cs b/LegendaryExplorer/LegendaryExplorer/Tools/PackageEditor/Experiments/PackageEditorExperimentsSquid.cs
index 1364eabd78..14ee2b16bd 100644
--- a/LegendaryExplorer/LegendaryExplorer/Tools/PackageEditor/Experiments/PackageEditorExperimentsSquid.cs
+++ b/LegendaryExplorer/LegendaryExplorer/Tools/PackageEditor/Experiments/PackageEditorExperimentsSquid.cs
@@ -27,6 +27,7 @@
using static LegendaryExplorerCore.Packages.CloningImportingAndRelinking.EntryImporter;
using static LegendaryExplorerCore.Unreal.PSA;
using Texture2D = LegendaryExplorerCore.Unreal.Classes.Texture2D;
+using MessageBox = Xceed.Wpf.Toolkit.MessageBox;
namespace LegendaryExplorer.Tools.PackageEditor.Experiments
{
diff --git a/LegendaryExplorer/LegendaryExplorer/Tools/PackageEditor/LECLDataEditorWindow.xaml b/LegendaryExplorer/LegendaryExplorer/Tools/PackageEditor/LECLDataEditorWindow.xaml
index 0cdfc03fcf..7c7f871162 100644
--- a/LegendaryExplorer/LegendaryExplorer/Tools/PackageEditor/LECLDataEditorWindow.xaml
+++ b/LegendaryExplorer/LegendaryExplorer/Tools/PackageEditor/LECLDataEditorWindow.xaml
@@ -1,4 +1,4 @@
-
-
+
diff --git a/LegendaryExplorer/LegendaryExplorer/Tools/PackageEditor/PackageEditorWindow.xaml b/LegendaryExplorer/LegendaryExplorer/Tools/PackageEditor/PackageEditorWindow.xaml
index 3614c09477..75ad123d2b 100644
--- a/LegendaryExplorer/LegendaryExplorer/Tools/PackageEditor/PackageEditorWindow.xaml
+++ b/LegendaryExplorer/LegendaryExplorer/Tools/PackageEditor/PackageEditorWindow.xaml
@@ -1,4 +1,4 @@
-
+
+
@@ -288,8 +344,8 @@
-
-
+
+
InstancedFullPath
@@ -305,7 +361,7 @@
-
+
InstancedFullPath
@@ -321,38 +377,38 @@
-
-
-
-
+
+
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
+
-
+
-
-
+
+
-
-
+
+
@@ -366,9 +422,9 @@
-
-
-
+
+
+
@@ -386,19 +442,19 @@
-
+
TestTrigger
-
+
@@ -406,12 +462,16 @@
-
+
@@ -427,13 +487,13 @@
+ Color="#0078D4" />
+ Color="White" />
+ Color="#505050" />
+ Color="White" />
@@ -453,11 +513,11 @@
-
+
-
-
-
+
+
+
Package Editor is the primary modding tool for editing game package files (.pcc) and contains
many embedded tools that appear as tabs on the right pane.
@@ -479,19 +539,19 @@
Check the menus for keyboard shortcuts. Check out CTRL+P: It pops out the currently viewed tab!
-
+
If you have issues, please report them to one of the following places:
- -
+ -
- The issues list on GitHub at
-
+
diff --git a/LegendaryExplorer/LegendaryExplorer/Tools/PackageEditor/PackageEditorWindow.xaml.cs b/LegendaryExplorer/LegendaryExplorer/Tools/PackageEditor/PackageEditorWindow.xaml.cs
index e3421d97b4..73c19f3492 100644
--- a/LegendaryExplorer/LegendaryExplorer/Tools/PackageEditor/PackageEditorWindow.xaml.cs
+++ b/LegendaryExplorer/LegendaryExplorer/Tools/PackageEditor/PackageEditorWindow.xaml.cs
@@ -13,6 +13,7 @@
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Threading;
+using System.Windows.Interop;
using GongSolutions.Wpf.DragDrop;
using LegendaryExplorer.Dialogs;
using LegendaryExplorer.DialogueEditor;
@@ -48,6 +49,11 @@
using LegendaryExplorerCore.UnrealScript.Language.Tree;
using LegendaryExplorer.Tools.AssetViewer;
using LegendaryExplorer.GameInterop;
+using Xceed.Wpf.Toolkit;
+using MessageBox = Xceed.Wpf.Toolkit.MessageBox;
+using MessageBoxResult = System.Windows.MessageBoxResult;
+using MessageBoxButton = System.Windows.MessageBoxButton;
+using MessageBoxImage = System.Windows.MessageBoxImage;
using LegendaryExplorer.Tools.ObjectReferenceViewer;
namespace LegendaryExplorer.Tools.PackageEditor
@@ -1808,7 +1814,7 @@ private void TrashEntryAndChildren()
(List itemsToTrash, IEntry entryWithReferences) = prevTask.Result;
if (entryWithReferences is not null)
{
- MessageBoxResult messageBoxResult = MessageBox.Show(this,
+ MessageBoxResult messageBoxResult = Xceed.Wpf.Toolkit.MessageBox.Show(this,
$"#{entryWithReferences.UIndex} {entryWithReferences.InstancedFullPath} is referenced by other entries! Use the \"{FindReferencesMenuText}\" option in the context menu to see the references. " +
"These references will be broken if you trash it! Are you sure you want to proceed?",
"Trash warning", MessageBoxButton.YesNo);
diff --git a/LegendaryExplorer/LegendaryExplorer/Tools/PackageEditor/TextureCreatorDialog.xaml b/LegendaryExplorer/LegendaryExplorer/Tools/PackageEditor/TextureCreatorDialog.xaml
index 4f51c9b157..8295485995 100644
--- a/LegendaryExplorer/LegendaryExplorer/Tools/PackageEditor/TextureCreatorDialog.xaml
+++ b/LegendaryExplorer/LegendaryExplorer/Tools/PackageEditor/TextureCreatorDialog.xaml
@@ -1,4 +1,4 @@
-
+
+
+
+
+
@@ -403,7 +410,7 @@
-
+
@@ -647,11 +654,11 @@
If you have issues with Pathfinding Editor, please report them to one of the following places:
- -
+ -
- The issues list on GitHub at
-
+
@@ -663,17 +670,17 @@
Visibility="{Binding Pcc, Converter={StaticResource NullVisibilityConverter}, FallbackValue=Visible}">
+ IsExpanded="False" Background="{DynamicResource ToolBoxAccentBrush}" Padding="2" MaxWidth="310">
+ Margin="0,0,4,0" Foreground="{DynamicResource {x:Static SystemColors.HighlightTextBrushKey}}">
-
+
-
+
diff --git a/LegendaryExplorer/LegendaryExplorer/Tools/PathfindingEditor/PathfindingEditorWindow.xaml.cs b/LegendaryExplorer/LegendaryExplorer/Tools/PathfindingEditor/PathfindingEditorWindow.xaml.cs
index 2fcf07a854..a7b097f23e 100644
--- a/LegendaryExplorer/LegendaryExplorer/Tools/PathfindingEditor/PathfindingEditorWindow.xaml.cs
+++ b/LegendaryExplorer/LegendaryExplorer/Tools/PathfindingEditor/PathfindingEditorWindow.xaml.cs
@@ -38,6 +38,7 @@
using System.Windows.Threading;
using DashStyle = System.Drawing.Drawing2D.DashStyle;
using RectangleF = System.Drawing.RectangleF;
+using MessageBox = Xceed.Wpf.Toolkit.MessageBox;
namespace LegendaryExplorer.Tools.PathfindingEditor
{
@@ -558,7 +559,23 @@ public bool SplineNodeSelected
#endregion
#region Load+I/O
- private static readonly System.Drawing.Color GraphEditorBackColor = System.Drawing.Color.FromArgb(130, 130, 130);
+ private Color _graphEditorBackColor = Color.FromArgb(Settings.PathfindingEditor_BackgroundColor);
+ public Color GraphEditorBackColor
+ {
+ get => _graphEditorBackColor;
+ set
+ {
+ if (_graphEditorBackColor != value)
+ {
+ _graphEditorBackColor = value;
+ if (graphEditor != null)
+ {
+ graphEditor.BackColor = value;
+ }
+ }
+ }
+ }
+
public PathfindingEditorWindow() : base("Pathfinding Editor")
{
DataContext = this;
@@ -643,6 +660,9 @@ public PathfindingEditorWindow(ExportEntry export) : this()
private void PathfindingEditorWPF_Loaded(object sender, RoutedEventArgs e)
{
+ // Initialize color picker with saved color
+ ClrPcker_Background.SelectedColor = GraphEditorBackColor.ToWPFColor();
+
if (FileQueuedForLoad != null || PackageQueuedForLoad != null)
{
Dispatcher.BeginInvoke(DispatcherPriority.Loaded, new Action(() =>
@@ -5067,6 +5087,17 @@ private void AddAllPathnodesToBioSquadCombat()
MessageBox.Show($"Added {message}.", "Success", MessageBoxButton.OK);
}
+ private void ColorPicker_SelectedColorChanged(object sender, RoutedPropertyChangedEventArgs e)
+ {
+ if (e.NewValue is not null)
+ {
+ var newColor = Color.FromArgb(e.NewValue.Value.A, e.NewValue.Value.R, e.NewValue.Value.G, e.NewValue.Value.B);
+ GraphEditorBackColor = newColor;
+ Settings.PathfindingEditor_BackgroundColor = newColor.ToArgb();
+ Settings.Save();
+ }
+ }
+
#region Busy
public override void SetBusy(string text = null)
diff --git a/LegendaryExplorer/LegendaryExplorer/Tools/PathfindingEditor/ReachSpecsPanel.xaml b/LegendaryExplorer/LegendaryExplorer/Tools/PathfindingEditor/ReachSpecsPanel.xaml
index 043c14326a..a55d8d9f31 100644
--- a/LegendaryExplorer/LegendaryExplorer/Tools/PathfindingEditor/ReachSpecsPanel.xaml
+++ b/LegendaryExplorer/LegendaryExplorer/Tools/PathfindingEditor/ReachSpecsPanel.xaml
@@ -1,4 +1,4 @@
-
-
+
diff --git a/LegendaryExplorer/LegendaryExplorer/Tools/PathfindingEditor/ReachSpecsPanel.xaml.cs b/LegendaryExplorer/LegendaryExplorer/Tools/PathfindingEditor/ReachSpecsPanel.xaml.cs
index b9de74b9b3..af6fd5ceb2 100644
--- a/LegendaryExplorer/LegendaryExplorer/Tools/PathfindingEditor/ReachSpecsPanel.xaml.cs
+++ b/LegendaryExplorer/LegendaryExplorer/Tools/PathfindingEditor/ReachSpecsPanel.xaml.cs
@@ -12,6 +12,7 @@
using LegendaryExplorerCore.Packages;
using LegendaryExplorerCore.Unreal;
using Microsoft.Win32;
+using MessageBox = Xceed.Wpf.Toolkit.MessageBox;
namespace LegendaryExplorer.Tools.PathfindingEditor
{
diff --git a/LegendaryExplorer/LegendaryExplorer/Tools/PathfindingEditor/ValidationPanel.xaml b/LegendaryExplorer/LegendaryExplorer/Tools/PathfindingEditor/ValidationPanel.xaml
index ab78e4ae1a..2391000862 100644
--- a/LegendaryExplorer/LegendaryExplorer/Tools/PathfindingEditor/ValidationPanel.xaml
+++ b/LegendaryExplorer/LegendaryExplorer/Tools/PathfindingEditor/ValidationPanel.xaml
@@ -1,4 +1,4 @@
-;
using InterpCurveFloat = LegendaryExplorerCore.Unreal.BinaryConverters.InterpCurve;
+using MessageBox = Xceed.Wpf.Toolkit.MessageBox;
namespace LegendaryExplorer.Tools.PathfindingEditor
{
diff --git a/LegendaryExplorer/LegendaryExplorer/Tools/PlotDatabase/PlotDatabaseWindow.xaml b/LegendaryExplorer/LegendaryExplorer/Tools/PlotDatabase/PlotDatabaseWindow.xaml
index dcd68401d1..7aa85c160f 100644
--- a/LegendaryExplorer/LegendaryExplorer/Tools/PlotDatabase/PlotDatabaseWindow.xaml
+++ b/LegendaryExplorer/LegendaryExplorer/Tools/PlotDatabase/PlotDatabaseWindow.xaml
@@ -1,4 +1,4 @@
-
-
@@ -65,7 +65,7 @@
-
+
@@ -229,7 +229,7 @@
-
+
@@ -312,8 +312,8 @@
-
-
+
+
@@ -344,8 +344,8 @@
-
-
+
+
@@ -380,8 +380,8 @@
-
-
+
+
diff --git a/LegendaryExplorer/LegendaryExplorer/Tools/PlotDatabase/PlotDatabaseWindow.xaml.cs b/LegendaryExplorer/LegendaryExplorer/Tools/PlotDatabase/PlotDatabaseWindow.xaml.cs
index 80451b56a5..ec9ad078aa 100644
--- a/LegendaryExplorer/LegendaryExplorer/Tools/PlotDatabase/PlotDatabaseWindow.xaml.cs
+++ b/LegendaryExplorer/LegendaryExplorer/Tools/PlotDatabase/PlotDatabaseWindow.xaml.cs
@@ -23,6 +23,7 @@
using LegendaryExplorerCore.PlotDatabase.Serialization;
using Microsoft.Win32;
using Newtonsoft.Json;
+using MessageBox = Xceed.Wpf.Toolkit.MessageBox;
namespace LegendaryExplorer.Tools.PlotDatabase
{
diff --git a/LegendaryExplorer/LegendaryExplorer/Tools/PlotEditor/CodexMapView.xaml b/LegendaryExplorer/LegendaryExplorer/Tools/PlotEditor/CodexMapView.xaml
index 22615a082d..3ea180ff38 100644
--- a/LegendaryExplorer/LegendaryExplorer/Tools/PlotEditor/CodexMapView.xaml
+++ b/LegendaryExplorer/LegendaryExplorer/Tools/PlotEditor/CodexMapView.xaml
@@ -1,4 +1,4 @@
-
-
@@ -35,7 +35,7 @@
-
+
@@ -51,6 +51,7 @@
@@ -183,6 +184,7 @@
@@ -293,4 +295,4 @@
-
\ No newline at end of file
+
diff --git a/LegendaryExplorer/LegendaryExplorer/Tools/PlotEditor/Dialogs/ChangeObjectIdDialog.xaml b/LegendaryExplorer/LegendaryExplorer/Tools/PlotEditor/Dialogs/ChangeObjectIdDialog.xaml
index cbbf8bd83d..39bd40b642 100644
--- a/LegendaryExplorer/LegendaryExplorer/Tools/PlotEditor/Dialogs/ChangeObjectIdDialog.xaml
+++ b/LegendaryExplorer/LegendaryExplorer/Tools/PlotEditor/Dialogs/ChangeObjectIdDialog.xaml
@@ -12,7 +12,7 @@
Title="Change Object Id"
Width="400" Height="200"
ContentRendered="ChangeObjectIdDialog_OnContentRendered">
-
+
@@ -24,7 +24,7 @@
@@ -69,4 +69,4 @@
-
\ No newline at end of file
+
diff --git a/LegendaryExplorer/LegendaryExplorer/Tools/PlotEditor/Dialogs/CopyObjectDialog.xaml b/LegendaryExplorer/LegendaryExplorer/Tools/PlotEditor/Dialogs/CopyObjectDialog.xaml
index b2285fe2e4..f86e4bf2c8 100644
--- a/LegendaryExplorer/LegendaryExplorer/Tools/PlotEditor/Dialogs/CopyObjectDialog.xaml
+++ b/LegendaryExplorer/LegendaryExplorer/Tools/PlotEditor/Dialogs/CopyObjectDialog.xaml
@@ -1,4 +1,4 @@
-
-
+
@@ -24,7 +24,7 @@
@@ -69,4 +69,4 @@
-
\ No newline at end of file
+
diff --git a/LegendaryExplorer/LegendaryExplorer/Tools/PlotEditor/Dialogs/NewObjectDialog.xaml b/LegendaryExplorer/LegendaryExplorer/Tools/PlotEditor/Dialogs/NewObjectDialog.xaml
index d6df275c53..4a0073af9e 100644
--- a/LegendaryExplorer/LegendaryExplorer/Tools/PlotEditor/Dialogs/NewObjectDialog.xaml
+++ b/LegendaryExplorer/LegendaryExplorer/Tools/PlotEditor/Dialogs/NewObjectDialog.xaml
@@ -1,4 +1,4 @@
-
-
+
@@ -24,7 +24,7 @@
@@ -69,4 +69,4 @@
-
\ No newline at end of file
+
diff --git a/LegendaryExplorer/LegendaryExplorer/Tools/PlotEditor/FindObjectUsagesView.xaml b/LegendaryExplorer/LegendaryExplorer/Tools/PlotEditor/FindObjectUsagesView.xaml
index 5e44c7d93c..61ca2511f9 100644
--- a/LegendaryExplorer/LegendaryExplorer/Tools/PlotEditor/FindObjectUsagesView.xaml
+++ b/LegendaryExplorer/LegendaryExplorer/Tools/PlotEditor/FindObjectUsagesView.xaml
@@ -1,4 +1,4 @@
-
-
\ No newline at end of file
+
diff --git a/LegendaryExplorer/LegendaryExplorer/Tools/PlotEditor/PlotEditorWindow.xaml b/LegendaryExplorer/LegendaryExplorer/Tools/PlotEditor/PlotEditorWindow.xaml
index 7a27d0029b..05319cf89a 100644
--- a/LegendaryExplorer/LegendaryExplorer/Tools/PlotEditor/PlotEditorWindow.xaml
+++ b/LegendaryExplorer/LegendaryExplorer/Tools/PlotEditor/PlotEditorWindow.xaml
@@ -1,4 +1,4 @@
-
-
\ No newline at end of file
+
diff --git a/LegendaryExplorer/LegendaryExplorer/Tools/PlotEditor/QuestMapView.xaml b/LegendaryExplorer/LegendaryExplorer/Tools/PlotEditor/QuestMapView.xaml
index a959676c09..ea4a18c1dd 100644
--- a/LegendaryExplorer/LegendaryExplorer/Tools/PlotEditor/QuestMapView.xaml
+++ b/LegendaryExplorer/LegendaryExplorer/Tools/PlotEditor/QuestMapView.xaml
@@ -1,4 +1,4 @@
-
-
@@ -35,7 +35,7 @@
-
+
@@ -50,6 +50,7 @@
@@ -105,7 +106,7 @@
-
+
@@ -121,6 +122,7 @@
@@ -237,6 +239,7 @@
@@ -352,6 +355,7 @@
@@ -448,6 +452,7 @@
@@ -466,10 +471,10 @@
-
+
-
@@ -304,11 +360,11 @@
If you have issues with Sequence Editor, please report them to one of the following places:
- -
+ -
- The issues list on GitHub at
-
+
@@ -324,15 +380,14 @@
ItemsSource="{Binding Sublinks}">
-
+
@@ -348,14 +403,6 @@
-
-
-
-
@@ -365,6 +412,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -389,10 +486,10 @@
-
@@ -435,4 +532,4 @@
-
\ No newline at end of file
+
diff --git a/LegendaryExplorer/LegendaryExplorer/Tools/Sequence Editor/SequenceEditorWPF.xaml.cs b/LegendaryExplorer/LegendaryExplorer/Tools/Sequence Editor/SequenceEditorWPF.xaml.cs
index d6023a0987..376a40cb6a 100644
--- a/LegendaryExplorer/LegendaryExplorer/Tools/Sequence Editor/SequenceEditorWPF.xaml.cs
+++ b/LegendaryExplorer/LegendaryExplorer/Tools/Sequence Editor/SequenceEditorWPF.xaml.cs
@@ -47,6 +47,12 @@
using OpenFileDialog = Microsoft.Win32.OpenFileDialog;
using SaveFileDialog = Microsoft.Win32.SaveFileDialog;
using LegendaryExplorer.Tools.PackageEditor;
+using Xceed.Wpf.Toolkit;
+using MessageBox = Xceed.Wpf.Toolkit.MessageBox;
+using MessageBoxResult = System.Windows.MessageBoxResult;
+using MessageBoxButton = System.Windows.MessageBoxButton;
+using MessageBoxImage = System.Windows.MessageBoxImage;
+using WindowStartupLocation = System.Windows.WindowStartupLocation;
namespace LegendaryExplorer.Tools.Sequence_Editor
{
@@ -111,6 +117,12 @@ public SequenceEditorWPF() : base("Sequence Editor")
RecentsController.InitRecentControl(Toolname, Recents_MenuItem, x => LoadFile(x));
+ // Load saved colors from settings (persisted user customizations)
+ LoadSavedColors();
+
+ // Subscribe to theme changes to update graph colors dynamically
+ ThemeManager.ThemeChanged += OnThemeChanged;
+
graphEditor = (SequenceGraphEditor)GraphHost.Child;
graphEditor.BackColor = GraphEditorBackColor;
graphEditor.Camera.MouseDown += backMouseDown_Handler;
@@ -137,6 +149,42 @@ public SequenceEditorWPF() : base("Sequence Editor")
AutoSaveView_MenuItem.IsChecked = Settings.SequenceEditor_AutoSaveViewV2;
ShowOutputNumbers_MenuItem.IsChecked = Settings.SequenceEditor_ShowOutputNumbers;
SObj.OutputNumbers = ShowOutputNumbers_MenuItem.IsChecked;
+
+ // Initialize color pickers with loaded colors
+ ClrPcker_Background.SelectedColor = GraphEditorBackColor.ToWPFColor();
+ ClrPcker_BoxFill.SelectedColor = BoxFillColor.ToWPFColor();
+ ClrPcker_TitleBox.SelectedColor = TitleBoxColor.ToWPFColor();
+ ClrPcker_CommentText.SelectedColor = CommentTextColor.ToWPFColor();
+ ClrPcker_BoxText.SelectedColor = BoxTextColor.ToWPFColor();
+ }
+
+ ///
+ /// Handles theme changes from the ThemeManager.
+ /// Resets graph colors to theme defaults when user switches between light/dark mode.
+ ///
+ private void OnThemeChanged(object sender, bool isDarkMode)
+ {
+ // Apply theme defaults (overrides any user customizations)
+ ApplyThemeDefaults();
+
+ // Update the graph editor background
+ if (graphEditor != null)
+ {
+ graphEditor.BackColor = GraphEditorBackColor;
+ }
+
+ // Update color pickers to reflect the new theme colors
+ ClrPcker_Background.SelectedColor = GraphEditorBackColor.ToWPFColor();
+ ClrPcker_BoxFill.SelectedColor = BoxFillColor.ToWPFColor();
+ ClrPcker_TitleBox.SelectedColor = TitleBoxColor.ToWPFColor();
+ ClrPcker_CommentText.SelectedColor = CommentTextColor.ToWPFColor();
+ ClrPcker_BoxText.SelectedColor = BoxTextColor.ToWPFColor();
+
+ // Refresh the view if there are objects loaded
+ if (CurrentObjects.Any())
+ {
+ RefreshView();
+ }
}
private void CreateCustomSequence(object obj)
@@ -1619,7 +1667,160 @@ public override void HandleUpdate(List updates)
private string FileQueuedForLoad;
private ExportEntry ExportQueuedForFocusing;
private bool AllowWindowRefocus = true;
- private static readonly Color GraphEditorBackColor = Color.FromArgb(167, 167, 167);
+
+ private Color _graphEditorBackColor = Color.FromArgb(79, 79, 79);
+ public Color GraphEditorBackColor
+ {
+ get => _graphEditorBackColor;
+ set
+ {
+ if (_graphEditorBackColor != value)
+ {
+ _graphEditorBackColor = value;
+ if (graphEditor != null)
+ {
+ graphEditor.BackColor = value;
+ if (CurrentObjects.Any())
+ {
+ RefreshView();
+ }
+ }
+ }
+ }
+ }
+
+ private Color _boxFillColor = Color.FromArgb(140, 140, 140);
+ public Color BoxFillColor
+ {
+ get => _boxFillColor;
+ set
+ {
+ if (_boxFillColor != value)
+ {
+ _boxFillColor = value;
+ SObj.NodeBrushColor = value;
+ if (CurrentObjects.Any())
+ {
+ RefreshView();
+ }
+ }
+ }
+ }
+
+ private Color _titleBoxColor = Color.FromArgb(112, 112, 112);
+ public Color TitleBoxColor
+ {
+ get => _titleBoxColor;
+ set
+ {
+ if (_titleBoxColor != value)
+ {
+ _titleBoxColor = value;
+ SObj.TitleBoxBrushColor = value;
+ if (CurrentObjects.Any())
+ {
+ RefreshView();
+ }
+ }
+ }
+ }
+
+ private Color _commentTextColor = Color.FromArgb(74, 63, 190);
+ public Color CommentTextColor
+ {
+ get => _commentTextColor;
+ set
+ {
+ if (_commentTextColor != value)
+ {
+ _commentTextColor = value;
+ SObj.CommentTextColor = value;
+ if (CurrentObjects.Any())
+ {
+ RefreshView();
+ }
+ }
+ }
+ }
+
+ private Color _boxTextColor = Color.FromArgb(0, 0, 0);
+ public Color BoxTextColor
+ {
+ get => _boxTextColor;
+ set
+ {
+ if (_boxTextColor != value)
+ {
+ _boxTextColor = value;
+ SObj.BoxTextColor = value;
+ if (CurrentObjects.Any())
+ {
+ RefreshView();
+ }
+ }
+ }
+ }
+
+
+
+
+
+
+
+
+ ///
+ /// Loads saved colors from settings. Called on initial window open to restore user customizations.
+ ///
+ private void LoadSavedColors()
+ {
+ // Load saved settings
+ _graphEditorBackColor = Color.FromArgb(Settings.SequenceEditor_BackgroundColor);
+ _boxFillColor = Color.FromArgb(Settings.SequenceEditor_BoxFillColor);
+ _titleBoxColor = Color.FromArgb(Settings.SequenceEditor_TitleBoxColor);
+ _commentTextColor = Color.FromArgb(Settings.SequenceEditor_CommentTextColor);
+ _boxTextColor = Color.FromArgb(Settings.SequenceEditor_BoxTextColor);
+
+ // Apply to static properties used by SObj
+ SObj.NodeBrushColor = _boxFillColor;
+ SObj.TitleBoxBrushColor = _titleBoxColor;
+ SObj.CommentTextColor = _commentTextColor;
+ SObj.BoxTextColor = _boxTextColor;
+ }
+
+ ///
+ /// Applies theme-appropriate default colors based on the current dark mode setting.
+ /// Called when user switches themes - overrides any user customizations.
+ /// User can then customize colors again via color pickers.
+ ///
+ private void ApplyThemeDefaults()
+ {
+ bool isDarkMode = Settings.Global_DarkMode_Enabled;
+
+ if (isDarkMode)
+ {
+ // Dark theme - Visual Studio dark mode inspired colors
+ _graphEditorBackColor = Color.FromArgb(30, 30, 30);
+ _boxFillColor = Color.FromArgb(45, 45, 48);
+ _titleBoxColor = Color.FromArgb(37, 37, 38);
+ _commentTextColor = Color.FromArgb(87, 166, 74);
+ _boxTextColor = Color.FromArgb(220, 220, 220);
+ }
+ else
+ {
+ // Light theme defaults
+ _graphEditorBackColor = Color.FromArgb(128, 128, 128);
+ _boxFillColor = Color.FromArgb(140, 140, 140);
+ _titleBoxColor = Color.FromArgb(112, 112, 112);
+ _commentTextColor = Color.FromArgb(25, 25, 112);
+ _boxTextColor = Color.FromArgb(255, 255, 255);
+ }
+
+ // Apply to static properties used by SObj
+ SObj.NodeBrushColor = _boxFillColor;
+ SObj.TitleBoxBrushColor = _titleBoxColor;
+ SObj.CommentTextColor = _commentTextColor;
+ SObj.BoxTextColor = _boxTextColor;
+ }
private void saveView(bool toFile = true)
{
@@ -2198,6 +2399,9 @@ private void SequenceEditorWPF_Closing(object sender, CancelEventArgs e)
Settings.SequenceEditor_AutoSaveViewV2 = AutoSaveView_MenuItem.IsChecked;
Settings.SequenceEditor_ShowOutputNumbers = SObj.OutputNumbers;
+ // Unsubscribe from theme changes to prevent memory leaks
+ ThemeManager.ThemeChanged -= OnThemeChanged;
+
//Code here remove these objects from leaking the window memory
graphEditor.Camera.MouseDown -= backMouseDown_Handler;
graphEditor.Camera.MouseUp -= back_MouseUp;
@@ -2267,6 +2471,40 @@ private void CloneObject_Clicked(object sender, RoutedEventArgs e)
}
}
+ private void CloneObjectWithLinks_Clicked(object sender, RoutedEventArgs e)
+ {
+ if (CurrentObjects_ListBox.SelectedItem is SObj obj)
+ {
+ // Save the link properties before cloning
+ var originalProps = obj.Export.GetProperties();
+ var outputLinks = originalProps.GetProp>("OutputLinks");
+ var variableLinks = originalProps.GetProp>("VariableLinks");
+ var eventLinks = originalProps.GetProp>("EventLinks");
+
+ // Clone the object (this may remove links due to the topLevel parameter)
+ ExportEntry clonedExport = KismetHelper.CloneObject(obj.Export, SelectedSequence);
+
+ // Restore the link properties to the cloned object
+ var clonedProps = clonedExport.GetProperties();
+ if (outputLinks != null)
+ {
+ clonedProps.AddOrReplaceProp(outputLinks);
+ }
+ if (variableLinks != null)
+ {
+ clonedProps.AddOrReplaceProp(variableLinks);
+ }
+ if (eventLinks != null)
+ {
+ clonedProps.AddOrReplaceProp(eventLinks);
+ }
+ clonedExport.WriteProperties(clonedProps);
+
+ customSaveData[clonedExport.UIndex] =
+ new PointF(graphEditor.Camera.ViewCenterX, graphEditor.Camera.ViewCenterY);
+ }
+ }
+
private void ContextMenu_Closed(object sender, RoutedEventArgs e)
{
graphEditor.AllowDragging();
@@ -3088,6 +3326,39 @@ private void AddSwitchOutlinksMenuItem_Clicked(object sender, RoutedEventArgs e)
}
}
}
+
+ private void ColorPicker_SelectedColorChanged(object sender, RoutedPropertyChangedEventArgs e)
+ {
+ var source = (Xceed.Wpf.Toolkit.ColorPicker)sender;
+ if (e.NewValue is not null)
+ {
+ var newColor = e.NewValue.Value.ToWinformsColor();
+ switch (source.Name)
+ {
+ case "ClrPcker_Background":
+ GraphEditorBackColor = newColor;
+ Settings.SequenceEditor_BackgroundColor = newColor.ToArgb();
+ break;
+ case "ClrPcker_BoxFill":
+ BoxFillColor = newColor;
+ Settings.SequenceEditor_BoxFillColor = newColor.ToArgb();
+ break;
+ case "ClrPcker_TitleBox":
+ TitleBoxColor = newColor;
+ Settings.SequenceEditor_TitleBoxColor = newColor.ToArgb();
+ break;
+ case "ClrPcker_CommentText":
+ CommentTextColor = newColor;
+ Settings.SequenceEditor_CommentTextColor = newColor.ToArgb();
+ break;
+ case "ClrPcker_BoxText":
+ BoxTextColor = newColor;
+ Settings.SequenceEditor_BoxTextColor = newColor.ToArgb();
+ break;
+ }
+ Settings.Save();
+ }
+ }
}
static class SequenceEditorExtensions
diff --git a/LegendaryExplorer/LegendaryExplorer/Tools/Sequence Editor/SequenceObjects.cs b/LegendaryExplorer/LegendaryExplorer/Tools/Sequence Editor/SequenceObjects.cs
index 2368b4dfc9..98fb3f2ede 100644
--- a/LegendaryExplorer/LegendaryExplorer/Tools/Sequence Editor/SequenceObjects.cs
+++ b/LegendaryExplorer/LegendaryExplorer/Tools/Sequence Editor/SequenceObjects.cs
@@ -46,32 +46,59 @@ public sealed class ActionEdge : SeqEdEdge
}
[DebuggerDisplay("SObj | #{UIndex}: {export.ObjectName.Instanced}")]
- public abstract class SObj : PNode, IDisposable
- {
- private static readonly Color CommentColor = Color.FromArgb(74, 63, 190);
- private static readonly Color IntColor = Color.FromArgb(34, 218, 218);//cyan
- private static readonly Color FloatColor = Color.FromArgb(23, 23, 213);//blue
- private static readonly Color BoolColor = Color.FromArgb(215, 37, 33); //red
- private static readonly Color ObjectColor = Color.FromArgb(219, 39, 217);//purple
- private static readonly Color InterpDataColor = Color.FromArgb(222, 123, 26);//orange
- private static readonly Color StringColor = Color.FromArgb(24, 219, 12);//lime green
- private static readonly Color VectorColor = Color.FromArgb(127, 123, 32);//dark gold
- private static readonly Color RotatorColor = Color.FromArgb(176, 97, 63);//burnt sienna
- protected static readonly Color EventColor = Color.FromArgb(214, 30, 28);
- protected static readonly Color TitleColor = Color.FromArgb(255, 255, 128);
- protected static readonly Brush TitleBoxBrush = new SolidBrush(Color.FromArgb(112, 112, 112));
- protected static readonly Brush MostlyTransparentBrush = new SolidBrush(Color.FromArgb(1, 255, 255, 255));
- protected static readonly Brush NodeBrush = new SolidBrush(Color.FromArgb(140, 140, 140));
- protected static readonly Pen SelectedPen = new(Color.FromArgb(255, 255, 0));
- protected static bool draggingOutlink;
- protected static bool draggingVarlink;
- protected static bool draggingEventlink;
- protected static PNode DragTarget;
- public static bool OutputNumbers;
-
- protected IMEPackage Pcc;
- protected SequenceGraphEditor g;
- public RectangleF PosAtDragStart;
+ public abstract class SObj : PNode, IDisposable
+ {
+ private static Color _commentColor = Color.FromArgb(74, 63, 190);
+ private static readonly Color IntColor = Color.FromArgb(34, 218, 218);//cyan
+ private static readonly Color FloatColor = Color.FromArgb(23, 23, 213);//blue
+ private static readonly Color BoolColor = Color.FromArgb(215, 37, 33); //red
+ private static readonly Color ObjectColor = Color.FromArgb(219, 39, 217);//purple
+ private static readonly Color InterpDataColor = Color.FromArgb(222, 123, 26);//orange
+ private static readonly Color StringColor = Color.FromArgb(24, 219, 12);//lime green
+ private static readonly Color VectorColor = Color.FromArgb(127, 123, 32);//dark gold
+ private static readonly Color RotatorColor = Color.FromArgb(176, 97, 63);//burnt sienna
+ protected static readonly Color EventColor = Color.FromArgb(214, 30, 28);
+ protected static readonly Color TitleColor = Color.FromArgb(255, 255, 128);
+ private static Color _titleBoxColor = Color.FromArgb(112, 112, 112);
+ protected static Brush TitleBoxBrush => new SolidBrush(_titleBoxColor);
+ protected static readonly Brush MostlyTransparentBrush = new SolidBrush(Color.FromArgb(1, 255, 255, 255));
+ private static Color _nodeBrushColor = Color.FromArgb(140, 140, 140);
+ protected static Brush NodeBrush => new SolidBrush(_nodeBrushColor);
+ protected static readonly Pen SelectedPen = new(Color.FromArgb(255, 255, 0));
+ private static Color _boxTextColor = Color.FromArgb(0, 0, 0); // Black text
+ protected static bool draggingOutlink;
+ protected static bool draggingVarlink;
+ protected static bool draggingEventlink;
+ protected static PNode DragTarget;
+ public static bool OutputNumbers;
+
+ public static Color NodeBrushColor
+ {
+ get => _nodeBrushColor;
+ set => _nodeBrushColor = value;
+ }
+
+ public static Color TitleBoxBrushColor
+ {
+ get => _titleBoxColor;
+ set => _titleBoxColor = value;
+ }
+
+ public static Color CommentTextColor
+ {
+ get => _commentColor;
+ set => _commentColor = value;
+ }
+
+ public static Color BoxTextColor
+ {
+ get => _boxTextColor;
+ set => _boxTextColor = value;
+ }
+
+ protected IMEPackage Pcc;
+ protected SequenceGraphEditor g;
+ public RectangleF PosAtDragStart;
protected ExportEntry export;
public ExportEntry Export => export;
@@ -83,16 +110,16 @@ public abstract class SObj : PNode, IDisposable
public string Comment => comment.Text;
- protected SObj(ExportEntry entry, SequenceGraphEditor grapheditor)
- {
- Pcc = entry.FileRef;
- export = entry;
- g = grapheditor;
- comment = new SText(GetComment(), CommentColor, false)
- {
- Pickable = false,
- };
- comment.Y = 0 - comment.Height;
+ protected SObj(ExportEntry entry, SequenceGraphEditor grapheditor)
+ {
+ Pcc = entry.FileRef;
+ export = entry;
+ g = grapheditor;
+ comment = new SText(GetComment(), _commentColor, false)
+ {
+ Pickable = false,
+ };
+ comment.Y = 0 - comment.Height;
AddChild(comment);
Pickable = true;
}
@@ -1571,69 +1598,69 @@ public override void Layout(float x, float y)
.Replace("SeqAct_", "").Replace("SeqCond_", "");
float starty = 8;
float w = 20;
- VarLinkBox = new PPath();
- for (int i = 0; i < Varlinks.Count; i++)
- {
- string d = string.Join(",", Varlinks[i].Links.Select(l => $"#{l}"));
- var t2 = new SText($"{d}\n{Varlinks[i].Desc}", x: w)
- {
- Pickable = false
- };
- w += t2.Width + 20;
- Varlinks[i].Node.TranslateBy(t2.X + t2.Width / 2, t2.Y + t2.Height);
- t2.AddChild(Varlinks[i].Node);
- VarLinkBox.AddChild(t2);
- }
- for (int i = 0; i < EventLinks.Count; i++)
- {
- string d = string.Join(",", EventLinks[i].Links.Select(l => $"#{l}"));
- var t2 = new SText($"{d}\n{EventLinks[i].Desc}", x: w)
- {
- Pickable = false
- };
- w += t2.Width + 20;
- EventLinks[i].Node.TranslateBy(t2.X + t2.Width / 2, t2.Y + t2.Height);
- t2.AddChild(EventLinks[i].Node);
- VarLinkBox.AddChild(t2);
- }
+ VarLinkBox = new PPath();
+ for (int i = 0; i < Varlinks.Count; i++)
+ {
+ string d = string.Join(",", Varlinks[i].Links.Select(l => $"#{l}"));
+ var t2 = new SText($"{d}\n{Varlinks[i].Desc}", BoxTextColor, x: w)
+ {
+ Pickable = false
+ };
+ w += t2.Width + 20;
+ Varlinks[i].Node.TranslateBy(t2.X + t2.Width / 2, t2.Y + t2.Height);
+ t2.AddChild(Varlinks[i].Node);
+ VarLinkBox.AddChild(t2);
+ }
+ for (int i = 0; i < EventLinks.Count; i++)
+ {
+ string d = string.Join(",", EventLinks[i].Links.Select(l => $"#{l}"));
+ var t2 = new SText($"{d}\n{EventLinks[i].Desc}", BoxTextColor, x: w)
+ {
+ Pickable = false
+ };
+ w += t2.Width + 20;
+ EventLinks[i].Node.TranslateBy(t2.X + t2.Width / 2, t2.Y + t2.Height);
+ t2.AddChild(EventLinks[i].Node);
+ VarLinkBox.AddChild(t2);
+ }
if (Varlinks.Any() || EventLinks.Any())
VarLinkBox.Height = VarLinkBox[0].Height;
VarLinkBox.Width = w;
VarLinkBox.Pickable = false;
- OutLinkBox = new PPath();
- float outW = 0;
- for (int i = 0; i < Outlinks.Count; i++)
- {
- string linkDesc = Outlinks[i].Desc;
- if (OutputNumbers && Outlinks[i].Links.Any())
- {
- linkDesc += $": {string.Join(",", Outlinks[i].Links.Select(l => $"#{l}"))}";
- }
- var t2 = new SText(linkDesc);
- if (t2.Width + 10 > outW) outW = t2.Width + 10;
- t2.SetBounds(0 - t2.Width, starty, t2.Width, t2.Height);
- starty += t2.Height;
- t2.Pickable = false;
- Outlinks[i].Node.TranslateBy(0, t2.Y + t2.Height / 2);
- t2.AddChild(Outlinks[i].Node);
- OutLinkBox.AddChild(t2);
- }
- OutLinkBox.Pickable = false;
- inputLinkBox = new PNode();
- GetInputLinks(properties);
- float inW = 0;
- float inY = 8;
- for (int i = 0; i < InLinks.Count; i++)
- {
- var t2 = new SText(InLinks[i].Desc, x: 3, y: inY);
- if (t2.Width > inW) inW = t2.Width;
- inY += t2.Height;
- t2.Pickable = false;
- PPath inLinkNode = InLinks[i].Node;
- inLinkNode.SetBounds(-10, t2.Y + t2.Height / 2 - 5, inLinkNode.Width, inLinkNode.Height);
- t2.AddChild(inLinkNode);
- inputLinkBox.AddChild(t2);
- }
+ OutLinkBox = new PPath();
+ float outW = 0;
+ for (int i = 0; i < Outlinks.Count; i++)
+ {
+ string linkDesc = Outlinks[i].Desc;
+ if (OutputNumbers && Outlinks[i].Links.Any())
+ {
+ linkDesc += $": {string.Join(",", Outlinks[i].Links.Select(l => $"#{l}"))}";
+ }
+ var t2 = new SText(linkDesc, BoxTextColor);
+ if (t2.Width + 10 > outW) outW = t2.Width + 10;
+ t2.SetBounds(0 - t2.Width, starty, t2.Width, t2.Height);
+ starty += t2.Height;
+ t2.Pickable = false;
+ Outlinks[i].Node.TranslateBy(0, t2.Y + t2.Height / 2);
+ t2.AddChild(Outlinks[i].Node);
+ OutLinkBox.AddChild(t2);
+ }
+ OutLinkBox.Pickable = false;
+ inputLinkBox = new PNode();
+ GetInputLinks(properties);
+ float inW = 0;
+ float inY = 8;
+ for (int i = 0; i < InLinks.Count; i++)
+ {
+ var t2 = new SText(InLinks[i].Desc, BoxTextColor, x: 3, y: inY);
+ if (t2.Width > inW) inW = t2.Width;
+ inY += t2.Height;
+ t2.Pickable = false;
+ PPath inLinkNode = InLinks[i].Node;
+ inLinkNode.SetBounds(-10, t2.Y + t2.Height / 2 - 5, inLinkNode.Width, inLinkNode.Height);
+ t2.AddChild(inLinkNode);
+ inputLinkBox.AddChild(t2);
+ }
inputLinkBox.Pickable = false;
if (inY > starty) starty = inY;
if (inW + outW + 10 > w) w = inW + outW + 10;
diff --git a/LegendaryExplorer/LegendaryExplorer/Tools/Soundplorer/SoundplorerWPF.xaml b/LegendaryExplorer/LegendaryExplorer/Tools/Soundplorer/SoundplorerWPF.xaml
index cd6e3e68eb..73cbc67434 100644
--- a/LegendaryExplorer/LegendaryExplorer/Tools/Soundplorer/SoundplorerWPF.xaml
+++ b/LegendaryExplorer/LegendaryExplorer/Tools/Soundplorer/SoundplorerWPF.xaml
@@ -1,4 +1,4 @@
-
ME3: Requires
-
+
LE1: Generating new streaming audio requires external use of
-
+
LE2 and LE3: Requires
-
+
@@ -144,12 +144,12 @@
If you have issues, please report them to one of the following places:
- -
+ -
- The issues list on GitHub at
-
+
@@ -177,7 +177,7 @@
-
+
diff --git a/LegendaryExplorer/LegendaryExplorer/Tools/Soundplorer/SoundplorerWPF.xaml.cs b/LegendaryExplorer/LegendaryExplorer/Tools/Soundplorer/SoundplorerWPF.xaml.cs
index 416aff7798..a15a2aea7d 100644
--- a/LegendaryExplorer/LegendaryExplorer/Tools/Soundplorer/SoundplorerWPF.xaml.cs
+++ b/LegendaryExplorer/LegendaryExplorer/Tools/Soundplorer/SoundplorerWPF.xaml.cs
@@ -34,6 +34,7 @@
using LegendaryExplorerCore.Sound.ISACT;
using AudioStreamHelper = LegendaryExplorer.UnrealExtensions.AudioStreamHelper;
using WwiseStream = LegendaryExplorerCore.Unreal.BinaryConverters.WwiseStream;
+using MessageBox = Xceed.Wpf.Toolkit.MessageBox;
namespace LegendaryExplorer.Tools.Soundplorer
{
diff --git a/LegendaryExplorer/LegendaryExplorer/Tools/TFCCompactor/TFCCompactorWindow.xaml b/LegendaryExplorer/LegendaryExplorer/Tools/TFCCompactor/TFCCompactorWindow.xaml
index a9d08f489d..5e15bed297 100644
--- a/LegendaryExplorer/LegendaryExplorer/Tools/TFCCompactor/TFCCompactorWindow.xaml
+++ b/LegendaryExplorer/LegendaryExplorer/Tools/TFCCompactor/TFCCompactorWindow.xaml
@@ -1,4 +1,4 @@
-
-
+
@@ -68,7 +68,7 @@
diff --git a/LegendaryExplorer/LegendaryExplorer/Tools/TextureStudio/MasterTextureSelector.xaml.cs b/LegendaryExplorer/LegendaryExplorer/Tools/TextureStudio/MasterTextureSelector.xaml.cs
index 19ce76c101..1a3b64ff62 100644
--- a/LegendaryExplorer/LegendaryExplorer/Tools/TextureStudio/MasterTextureSelector.xaml.cs
+++ b/LegendaryExplorer/LegendaryExplorer/Tools/TextureStudio/MasterTextureSelector.xaml.cs
@@ -20,6 +20,7 @@
using LegendaryExplorerCore.Unreal.BinaryConverters;
using Microsoft.Win32;
using Image = LegendaryExplorerCore.Textures.Image;
+using MessageBox = Xceed.Wpf.Toolkit.MessageBox;
namespace LegendaryExplorer.Tools.TextureStudio
{
diff --git a/LegendaryExplorer/LegendaryExplorer/Tools/TextureStudio/TextureStudioWindow.xaml b/LegendaryExplorer/LegendaryExplorer/Tools/TextureStudio/TextureStudioWindow.xaml
index 0c1496ab0f..2514a5cc38 100644
--- a/LegendaryExplorer/LegendaryExplorer/Tools/TextureStudio/TextureStudioWindow.xaml
+++ b/LegendaryExplorer/LegendaryExplorer/Tools/TextureStudio/TextureStudioWindow.xaml
@@ -1,4 +1,4 @@
-
-
+
Default - Female
Default - Male
ES - Female
@@ -68,7 +68,7 @@
PL - Male
PLPC - Female
PLPC - Male
-
+
@@ -88,7 +88,7 @@
-
+
INT
ESN
DEU
@@ -97,7 +97,7 @@
POL
RUS
JPN
-
+
@@ -116,7 +116,7 @@
-
+
INT
ESN
DEU
@@ -125,7 +125,7 @@
POL
RUS
JPN
-
+
@@ -181,7 +181,7 @@
-
+
Default - Female
Default - Male
ES - Female
@@ -198,7 +198,7 @@
JA - Male
RA - Female
RA - Male
-
+
@@ -218,7 +218,7 @@
-
+
INT
ESN
DEU
@@ -227,7 +227,7 @@
POL
RUS
JPN
-
+
@@ -246,7 +246,7 @@
-
+
INT
ESN
DEU
@@ -255,7 +255,7 @@
POL
RUS
JPN
-
+
diff --git a/LegendaryExplorer/LegendaryExplorer/Tools/TlkManager/TLKManagerWPF.xaml.cs b/LegendaryExplorer/LegendaryExplorer/Tools/TlkManager/TLKManagerWPF.xaml.cs
index f976373368..e53c08d24d 100644
--- a/LegendaryExplorer/LegendaryExplorer/Tools/TlkManager/TLKManagerWPF.xaml.cs
+++ b/LegendaryExplorer/LegendaryExplorer/Tools/TlkManager/TLKManagerWPF.xaml.cs
@@ -19,6 +19,7 @@
using LegendaryExplorerCore.Misc;
using LegendaryExplorerCore.Packages;
using LegendaryExplorerCore.TLK;
+using MessageBox = Xceed.Wpf.Toolkit.MessageBox;
namespace LegendaryExplorer.Tools.TlkManagerNS
{
diff --git a/LegendaryExplorer/LegendaryExplorer/Tools/TlkManager/TLKManagerWPF_ExportReplaceDialog.xaml b/LegendaryExplorer/LegendaryExplorer/Tools/TlkManager/TLKManagerWPF_ExportReplaceDialog.xaml
index 6cc4591d06..728e2e7268 100644
--- a/LegendaryExplorer/LegendaryExplorer/Tools/TlkManager/TLKManagerWPF_ExportReplaceDialog.xaml
+++ b/LegendaryExplorer/LegendaryExplorer/Tools/TlkManager/TLKManagerWPF_ExportReplaceDialog.xaml
@@ -1,4 +1,4 @@
-
-
+
diff --git a/LegendaryExplorer/LegendaryExplorer/Tools/WwiseEditor/WwiseEditorWindow.xaml.cs b/LegendaryExplorer/LegendaryExplorer/Tools/WwiseEditor/WwiseEditorWindow.xaml.cs
index a9e7556179..51691d5755 100644
--- a/LegendaryExplorer/LegendaryExplorer/Tools/WwiseEditor/WwiseEditorWindow.xaml.cs
+++ b/LegendaryExplorer/LegendaryExplorer/Tools/WwiseEditor/WwiseEditorWindow.xaml.cs
@@ -31,6 +31,7 @@
using Color = System.Drawing.Color;
using Image = System.Drawing.Image;
using Path = System.IO.Path;
+using MessageBox = Xceed.Wpf.Toolkit.MessageBox;
namespace LegendaryExplorer.Tools.WwiseEditor
{
diff --git a/LegendaryExplorer/LegendaryExplorer/ToolsetDev/FileHexViewer.xaml b/LegendaryExplorer/LegendaryExplorer/ToolsetDev/FileHexViewer.xaml
index edec58fb74..eadf73dcce 100644
--- a/LegendaryExplorer/LegendaryExplorer/ToolsetDev/FileHexViewer.xaml
+++ b/LegendaryExplorer/LegendaryExplorer/ToolsetDev/FileHexViewer.xaml
@@ -1,4 +1,4 @@
-
-
+
@@ -51,8 +51,8 @@
-
-
+
+
@@ -68,13 +68,13 @@
-
-
+
+
-
+
@@ -205,14 +205,14 @@
-
+
-
-
-
-
+
+
+
+
diff --git a/LegendaryExplorer/LegendaryExplorer/UserControls/ExportLoaderControls/CurveEditor.xaml.cs b/LegendaryExplorer/LegendaryExplorer/UserControls/ExportLoaderControls/CurveEditor.xaml.cs
index 1291911c13..05914b7e58 100644
--- a/LegendaryExplorer/LegendaryExplorer/UserControls/ExportLoaderControls/CurveEditor.xaml.cs
+++ b/LegendaryExplorer/LegendaryExplorer/UserControls/ExportLoaderControls/CurveEditor.xaml.cs
@@ -14,6 +14,7 @@
using LegendaryExplorerCore.Packages;
using LegendaryExplorerCore.Unreal;
using Microsoft.WindowsAPICodePack.Dialogs;
+using MessageBox = Xceed.Wpf.Toolkit.MessageBox;
namespace LegendaryExplorer.UserControls.ExportLoaderControls
{
diff --git a/LegendaryExplorer/LegendaryExplorer/UserControls/ExportLoaderControls/EntryMetadataExportLoader.xaml b/LegendaryExplorer/LegendaryExplorer/UserControls/ExportLoaderControls/EntryMetadataExportLoader.xaml
index b9e7717fca..cf456c2554 100644
--- a/LegendaryExplorer/LegendaryExplorer/UserControls/ExportLoaderControls/EntryMetadataExportLoader.xaml
+++ b/LegendaryExplorer/LegendaryExplorer/UserControls/ExportLoaderControls/EntryMetadataExportLoader.xaml
@@ -1,4 +1,4 @@
-
-
@@ -125,16 +128,19 @@
-
-
-
-
-
-
+
+
+
+
-
-
-
+
+
diff --git a/LegendaryExplorer/LegendaryExplorer/UserControls/ExportLoaderControls/EntryMetadataExportLoader.xaml.cs b/LegendaryExplorer/LegendaryExplorer/UserControls/ExportLoaderControls/EntryMetadataExportLoader.xaml.cs
index 2e7424bf4a..7fcea995fe 100644
--- a/LegendaryExplorer/LegendaryExplorer/UserControls/ExportLoaderControls/EntryMetadataExportLoader.xaml.cs
+++ b/LegendaryExplorer/LegendaryExplorer/UserControls/ExportLoaderControls/EntryMetadataExportLoader.xaml.cs
@@ -18,8 +18,10 @@
using LegendaryExplorerCore.Packages.CloningImportingAndRelinking;
using LegendaryExplorerCore.Unreal;
using LegendaryExplorerCore.UnrealScript.Documentation;
-using Xceed.Wpf.Toolkit.Primitives;
+using LegendaryExplorer.SharedUI.Controls;
+using System.Windows.Controls.Primitives;
using static LegendaryExplorerCore.Unreal.UnrealFlags;
+using MessageBox = Xceed.Wpf.Toolkit.MessageBox;
namespace LegendaryExplorer.UserControls.ExportLoaderControls
{
@@ -45,6 +47,7 @@ public partial class EntryMetadataExportLoader : ExportLoaderControl
private const int HEADER_OFFSET_IMP_IDXLINK = 0x10;
private const int HEADER_OFFSET_IMP_IDXOBJECTNAME = 0x14;
private const int HEADER_OFFSET_IMP_IDXPACKAGEFILE = 0x0;
+
private IEntry _currentLoadedEntry;
public IEntry CurrentLoadedEntry
{
@@ -533,11 +536,11 @@ internal void ClearMetadataPane()
//InfoTab_Archetype_ComboBox.Items.Clear();
InfoTab_Archetype_ComboBox.SelectedItem = null;
InfoTab_Flags_ComboBox.ItemsSource = null;
- InfoTab_Flags_ComboBox.SelectedItem = null;
+ InfoTab_Flags_ComboBox.SelectedValue = null;
InfoTab_ExportFlags_ComboBox.ItemsSource = null;
- InfoTab_ExportFlags_ComboBox.SelectedItem = null;
+ InfoTab_ExportFlags_ComboBox.SelectedValue = null;
InfoTab_PackageFlags_ComboBox.ItemsSource = null;
- InfoTab_PackageFlags_ComboBox.SelectedItem = null;
+ InfoTab_PackageFlags_ComboBox.SelectedValue = null;
InfoTab_ExportDataSize_TextBox.Text = null;
InfoTab_ExportOffsetHex_TextBox.Text = null;
InfoTab_ExportOffsetDec_TextBox.Text = null;
@@ -791,7 +794,7 @@ private void InfoTab_Flags_ComboBox_ItemSelectionChanged(object sender, ItemSele
EObjectFlags newFlags = 0U;
foreach (object flag in InfoTab_Flags_ComboBox.Items)
{
- if (InfoTab_Flags_ComboBox.ItemContainerGenerator.ContainerFromItem(flag) is SelectorItem { IsSelected: true })
+ if (InfoTab_Flags_ComboBox.ItemContainerGenerator.ContainerFromItem(flag) is ListBoxItem { IsSelected: true })
{
newFlags |= (EObjectFlags)flag;
}
@@ -814,7 +817,7 @@ private void InfoTab_ExportFlags_ComboBox_ItemSelectionChanged(object sender, It
EExportFlags newFlags = 0U;
foreach (object flag in InfoTab_ExportFlags_ComboBox.Items)
{
- if (InfoTab_ExportFlags_ComboBox.ItemContainerGenerator.ContainerFromItem(flag) is SelectorItem { IsSelected: true })
+ if (InfoTab_ExportFlags_ComboBox.ItemContainerGenerator.ContainerFromItem(flag) is ListBoxItem { IsSelected: true })
{
newFlags |= (EExportFlags)flag;
}
@@ -841,6 +844,9 @@ private void MetadataEditor_Loaded(object sender, RoutedEventArgs e)
Header_Hexbox.SelectionStartChanged += hb1_SelectionChanged;
Header_Hexbox.SelectionLengthChanged += hb1_SelectionChanged;
+
+ // Register HexBox for theme management
+ Misc.ThemeManager.RegisterHexBox(Header_Hexbox);
}
}
@@ -1003,7 +1009,7 @@ private void InfoTab_PackageFlags_ComboBox_ItemSelectionChanged(object sender, I
EPackageFlags newFlags = 0U;
foreach (object flag in InfoTab_PackageFlags_ComboBox.Items)
{
- if (InfoTab_PackageFlags_ComboBox.ItemContainerGenerator.ContainerFromItem(flag) is SelectorItem { IsSelected: true })
+ if (InfoTab_PackageFlags_ComboBox.ItemContainerGenerator.ContainerFromItem(flag) is ListBoxItem { IsSelected: true })
{
newFlags |= (EPackageFlags)flag;
}
diff --git a/LegendaryExplorer/LegendaryExplorer/UserControls/ExportLoaderControls/ExportLoaderHostedWindow.xaml b/LegendaryExplorer/LegendaryExplorer/UserControls/ExportLoaderControls/ExportLoaderHostedWindow.xaml
index 7f8a910eb4..863b09e066 100644
--- a/LegendaryExplorer/LegendaryExplorer/UserControls/ExportLoaderControls/ExportLoaderHostedWindow.xaml
+++ b/LegendaryExplorer/LegendaryExplorer/UserControls/ExportLoaderControls/ExportLoaderHostedWindow.xaml
@@ -1,4 +1,4 @@
-
-
\ No newline at end of file
+
diff --git a/LegendaryExplorer/LegendaryExplorer/UserControls/ExportLoaderControls/ExportLoaderHostedWindow.xaml.cs b/LegendaryExplorer/LegendaryExplorer/UserControls/ExportLoaderControls/ExportLoaderHostedWindow.xaml.cs
index eec4eae46e..bee4d011ed 100644
--- a/LegendaryExplorer/LegendaryExplorer/UserControls/ExportLoaderControls/ExportLoaderHostedWindow.xaml.cs
+++ b/LegendaryExplorer/LegendaryExplorer/UserControls/ExportLoaderControls/ExportLoaderHostedWindow.xaml.cs
@@ -14,6 +14,7 @@
using LegendaryExplorerCore.Misc;
using LegendaryExplorerCore.Packages;
using Microsoft.Win32;
+using MessageBox = Xceed.Wpf.Toolkit.MessageBox;
namespace LegendaryExplorer.UserControls.ExportLoaderControls
{
diff --git a/LegendaryExplorer/LegendaryExplorer/UserControls/ExportLoaderControls/FaceFXAnimSetEditorControl.xaml b/LegendaryExplorer/LegendaryExplorer/UserControls/ExportLoaderControls/FaceFXAnimSetEditorControl.xaml
index d7da47bdd4..f1cc326f05 100644
--- a/LegendaryExplorer/LegendaryExplorer/UserControls/ExportLoaderControls/FaceFXAnimSetEditorControl.xaml
+++ b/LegendaryExplorer/LegendaryExplorer/UserControls/ExportLoaderControls/FaceFXAnimSetEditorControl.xaml
@@ -1,4 +1,4 @@
-
+ /// Adds FaceFX lines from WwiseEvents in a selected package folder.
+ /// Female WwiseEvents (containing "f_play") are only added to female FaceFX assets (ending in "_f").
+ /// Male WwiseEvents (containing "m_play") are only added to male FaceFX assets (ending in "_m").
+ ///
+ private void AddAudioFromFolder()
+ {
+ if (CurrentLoadedExport == null) return;
+
+ // Determine if this is a female or male FaceFX asset
+ bool isFemaleAsset = CurrentLoadedExport.ObjectName.Name.EndsWith("_F", StringComparison.OrdinalIgnoreCase);
+ bool isMaleAsset = CurrentLoadedExport.ObjectName.Name.EndsWith("_M", StringComparison.OrdinalIgnoreCase);
+
+ if (!isFemaleAsset && !isMaleAsset)
+ {
+ MessageBox.Show("This FaceFX asset does not end with '_F' or '_M'. Cannot determine gender for audio filtering.",
+ "Error", MessageBoxButton.OK, MessageBoxImage.Warning);
+ return;
+ }
+
+ // Prompt user to select a folder (Package export) in the package
+ var folderExport = EntrySelector.GetEntry(Window.GetWindow(this), Pcc,
+ "Select the folder containing WwiseEvents:",
+ exp => exp.ClassName == "Package");
+
+ if (folderExport == null) return;
+
+ // Get all WwiseEvents under the selected folder
+ var entryTree = new EntryTree(Pcc);
+ var allEntriesInFolder = entryTree.FlattenTreeOf(folderExport, includeRoot: false);
+ var wwiseEvents = allEntriesInFolder
+ .OfType()
+ .Where(exp => exp.ClassName == "WwiseEvent")
+ .ToList();
+
+ if (wwiseEvents.Count == 0)
+ {
+ MessageBox.Show("No WwiseEvents found in the selected folder.", "No Events", MessageBoxButton.OK, MessageBoxImage.Information);
+ return;
+ }
+
+ // Filter WwiseEvents based on gender
+ List filteredEvents;
+ if (isFemaleAsset)
+ {
+ filteredEvents = wwiseEvents.Where(e => e.ObjectName.Name.Contains("f_play", StringComparison.OrdinalIgnoreCase) ||
+ e.ObjectName.Name.Contains("f_Play", StringComparison.OrdinalIgnoreCase)).ToList();
+ }
+ else
+ {
+ filteredEvents = wwiseEvents.Where(e => e.ObjectName.Name.Contains("m_play", StringComparison.OrdinalIgnoreCase) ||
+ e.ObjectName.Name.Contains("m_Play", StringComparison.OrdinalIgnoreCase)).ToList();
+ }
+
+ if (filteredEvents.Count == 0)
+ {
+ string genderType = isFemaleAsset ? "female (f_play)" : "male (m_play)";
+ MessageBox.Show($"No {genderType} WwiseEvents found in the selected folder.", "No Matching Events", MessageBoxButton.OK, MessageBoxImage.Information);
+ return;
+ }
+
+ // Get or create ReferencedSoundCues property
+ var referencedSoundCues = CurrentLoadedExport.GetProperty>("ReferencedSoundCues")
+ ?? new ArrayProperty("ReferencedSoundCues");
+
+ // Build a set of already referenced WwiseEvent UIndexes for quick lookup
+ var existingReferences = new HashSet(referencedSoundCues.Select(op => op.Value));
+
+ int linesAdded = 0;
+ int skippedDuplicates = 0;
+ int lineIndex = FaceFX.Lines.Count;
+
+ foreach (var wwiseEvent in filteredEvents)
+ {
+ // Check if this WwiseEvent is already in ReferencedSoundCues
+ if (existingReferences.Contains(wwiseEvent.UIndex))
+ {
+ skippedDuplicates++;
+ continue; // Skip - already referenced
+ }
+
+ // Extract TLK ID from WwiseEvent name (e.g., "VO_123456_f_Play" -> 123456)
+ string eventName = wwiseEvent.ObjectName.Name;
+ int tlkID = ExtractTlkIdFromWwiseEventName(eventName);
+
+ if (tlkID <= 0)
+ {
+ continue; // Skip if we can't extract a valid TLK ID
+ }
+
+ // Check if a line with this TLK ID already exists
+ if (Lines.Any(l => l.TLKID == tlkID))
+ {
+ skippedDuplicates++;
+ continue; // Skip duplicates
+ }
+
+ // Create the line name (e.g., "FXA_123456_F" or "FXA_123456_M")
+ string lineName = $"FXA_{tlkID}_{(isFemaleAsset ? "F" : "M")}";
+ string lineId = tlkID.ToString();
+
+ // Create the new FaceFX line
+ var line = new FaceFXLine
+ {
+ NameIndex = FaceFX.Names.FindOrAdd(lineName),
+ NameAsString = lineName,
+ AnimationNames = new List(),
+ Points = new List(),
+ NumKeys = new List(),
+ FadeInTime = 0.16f,
+ FadeOutTime = 0.22f,
+ Path = wwiseEvent.InstancedFullPath,
+ ID = lineId,
+ Index = lineIndex
+ };
+
+ // Add to ReferencedSoundCues and track it
+ referencedSoundCues.Add(new ObjectProperty(wwiseEvent.UIndex));
+ existingReferences.Add(wwiseEvent.UIndex);
+
+ // Create the line entry for UI
+ var lineEntry = new FaceFXLineEntry(line)
+ {
+ IsMale = !isFemaleAsset,
+ TLKID = tlkID,
+ TLKString = TLKManagerWPF.GlobalFindStrRefbyID(tlkID, Pcc)
+ };
+
+ FaceFX.Lines.Add(line);
+ Lines.Add(lineEntry);
+
+ lineIndex++;
+ linesAdded++;
+ }
+
+ if (linesAdded > 0)
+ {
+ // Write the ReferencedSoundCues property
+ CurrentLoadedExport.WriteProperty(referencedSoundCues);
+
+ // Save changes to the binary
+ CurrentLoadedExport.WriteBinary(FaceFX.Binary);
+
+ string message = $"Successfully added {linesAdded} line(s) from WwiseEvents.";
+ if (skippedDuplicates > 0)
+ {
+ message += $"\n{skippedDuplicates} duplicate(s) were skipped.";
+ }
+ MessageBox.Show(message, "Lines Added", MessageBoxButton.OK, MessageBoxImage.Information);
+ }
+ else
+ {
+ MessageBox.Show($"No new lines were added. {skippedDuplicates} WwiseEvent(s) already have corresponding lines.",
+ "No Lines Added", MessageBoxButton.OK, MessageBoxImage.Information);
+ }
+ }
+
+ ///
+ /// Extracts the TLK ID from a WwiseEvent name.
+ /// Expected format: "VO_123456_f_Play" or similar patterns containing the numeric TLK ID.
+ ///
+ private static int ExtractTlkIdFromWwiseEventName(string eventName)
+ {
+ // Try to find a pattern like "VO_123456" or just extract digits
+ var match = Regex.Match(eventName, @"VO_(\d+)", RegexOptions.IgnoreCase);
+ if (match.Success && int.TryParse(match.Groups[1].Value, out int tlkId))
+ {
+ return tlkId;
+ }
+
+ // Fallback: try to find any sequence of 6+ digits
+ match = Regex.Match(eventName, @"(\d{6,})");
+ if (match.Success && int.TryParse(match.Groups[1].Value, out tlkId))
+ {
+ return tlkId;
+ }
+
+ return -1;
+ }
+
+ private void AutoFaceFXGeneration_Click(object sender, RoutedEventArgs e)
+ {
+ if (SelectedLineEntry == null || SelectedLine == null)
+ {
+ MessageBox.Show("Please select a line first.", "No Line Selected", MessageBoxButton.OK, MessageBoxImage.Warning);
+ return;
+ }
+
+ // Find the audio export for this line
+ var audioExport = FindVoiceStreamFromExport(SelectedLineEntry);
+
+ // Create and show the dialog
+ var dialog = new Tools.FaceFXEditor.AutoFaceFXGenerator.AutoFaceFXGenerationDialog(
+ FaceFX,
+ SelectedLine,
+ SelectedLineEntry.TLKID,
+ SelectedLineEntry.TLKString,
+ audioExport,
+ Window.GetWindow(this));
+
+ if (dialog.ShowDialog() == true && dialog.WasGenerated)
+ {
+ // Write the binary directly since the generator modified the line object
+ // We must do this BEFORE UpdateAnimListBox because SaveChanges reads from the UI Animations collection
+ CurrentLoadedExport?.WriteBinary(FaceFX.Binary);
+
+ // Refresh the UI to show the new animations
+ UpdateAnimListBox();
+ SelectedLineEntry.UpdateLength();
+
+ MessageBox.Show("FaceFX animations generated successfully!", "Generation Complete", MessageBoxButton.OK, MessageBoxImage.Information);
+ }
+ }
+
+ private void BulkGenerateFaceFX_Click(object sender, RoutedEventArgs e)
+ {
+ if (Lines.Count == 0)
+ {
+ MessageBox.Show("No lines in this FaceFX asset.", "No Lines", MessageBoxButton.OK, MessageBoxImage.Warning);
+ return;
+ }
+
+ // Show bulk generation options dialog
+ var bulkDialog = new Tools.FaceFXEditor.AutoFaceFXGenerator.BulkFaceFXGenerationDialog(Lines.Count, Window.GetWindow(this));
+ if (bulkDialog.ShowDialog() != true || !bulkDialog.Confirmed)
+ return;
+
+ var selectedSpecies = bulkDialog.SelectedSpeciesEnum;
+ var lipSyncIntensity = bulkDialog.LipSyncIntensity;
+
+ int successCount = 0;
+ int skipCount = 0;
+ int errorCount = 0;
+ var errors = new List();
+
+ foreach (var lineEntry in Lines)
+ {
+ if (string.IsNullOrWhiteSpace(lineEntry.TLKString))
+ {
+ skipCount++;
+ continue;
+ }
+
+ try
+ {
+ var audioExport = FindVoiceStreamFromExport(lineEntry);
+
+ var options = new Tools.FaceFXEditor.AutoFaceFXGenerator.FaceFXGenerationOptions
+ {
+ CharacterType = Tools.FaceFXEditor.AutoFaceFXGenerator.CharacterType.HumanFemale,
+ Species = selectedSpecies,
+ GenerateJawAnimation = true,
+ GenerateBlinkAnimation = true,
+ GenerateEyebrowAnimation = true,
+ GenerateHeadMovement = false,
+ LipSyncIntensity = lipSyncIntensity,
+ BlinkFrequency = 0.2f,
+ UseAudioAmplitude = true,
+ FxaData = null,
+ UseTextFallback = true
+ };
+
+ var generator = new Tools.FaceFXEditor.AutoFaceFXGenerator.FaceFXGenerator(
+ FaceFX, lineEntry.Line, lineEntry.TLKString, audioExport, options);
+
+ if (generator.Generate())
+ {
+ successCount++;
+ lineEntry.UpdateLength();
+ }
+ else
+ {
+ errorCount++;
+ errors.Add($"{lineEntry.Name}: {generator.LastError ?? "Unknown error"}");
+ }
+ }
+ catch (Exception ex)
+ {
+ errorCount++;
+ errors.Add($"{lineEntry.Name}: {ex.Message}");
+ }
+ }
+
+ CurrentLoadedExport?.WriteBinary(FaceFX.Binary);
+ if (SelectedLineEntry != null) UpdateAnimListBox();
+
+ string message = $"Bulk generation complete.\n\nSuccess: {successCount}\nSkipped (no TLK text): {skipCount}\nErrors: {errorCount}";
+ if (errors.Count > 0 && errors.Count <= 10)
+ message += "\n\nErrors:\n" + string.Join("\n", errors);
+ else if (errors.Count > 10)
+ message += $"\n\nFirst 10 errors:\n" + string.Join("\n", errors.Take(10));
+
+ MessageBox.Show(message, "Bulk Generation Complete", MessageBoxButton.OK,
+ errorCount > 0 ? MessageBoxImage.Warning : MessageBoxImage.Information);
+ }
+
+ private void BulkClearAnimations_Click(object sender, RoutedEventArgs e)
+ {
+ if (Lines.Count == 0)
+ {
+ MessageBox.Show("No lines in this FaceFX asset.", "No Lines", MessageBoxButton.OK, MessageBoxImage.Warning);
+ return;
+ }
+
+ var result = MessageBox.Show(
+ $"This will clear ALL animations from ALL {Lines.Count} lines in this FaceFX asset.\n\n" +
+ "This includes lip sync, blink, eyebrow, head movement, and all other animations.\n\n" +
+ "This operation cannot be undone. Continue?",
+ "Bulk Clear All Animations",
+ MessageBoxButton.YesNo,
+ MessageBoxImage.Warning);
+
+ if (result != MessageBoxResult.Yes)
+ return;
+
+ int clearedCount = 0;
+ foreach (var lineEntry in Lines)
+ {
+ if (lineEntry.Line != null)
+ {
+ lineEntry.Line.AnimationNames?.Clear();
+ lineEntry.Line.NumKeys?.Clear();
+ lineEntry.Line.Points?.Clear();
+ lineEntry.UpdateLength();
+ clearedCount++;
+ }
+ }
+
+ CurrentLoadedExport?.WriteBinary(FaceFX.Binary);
+ if (SelectedLineEntry != null) UpdateAnimListBox();
+
+ MessageBox.Show($"Cleared all animations from {clearedCount} lines.", "Bulk Clear Complete",
+ MessageBoxButton.OK, MessageBoxImage.Information);
+ }
+
+ private void BulkDeleteAllLines_Click(object sender, RoutedEventArgs e)
+ {
+ if (Lines.Count == 0)
+ {
+ MessageBox.Show("No lines in this FaceFX asset.", "No Lines", MessageBoxButton.OK, MessageBoxImage.Warning);
+ return;
+ }
+
+ var result = MessageBox.Show(
+ $"This will DELETE ALL {Lines.Count} lines from this FaceFX asset.\n\n" +
+ "This operation cannot be undone. Continue?",
+ "Bulk Delete All Lines",
+ MessageBoxButton.YesNo,
+ MessageBoxImage.Warning);
+
+ if (result != MessageBoxResult.Yes)
+ return;
+
+ int deletedCount = Lines.Count;
+ FaceFX.Lines.Clear();
+ Lines.Clear();
+ SaveChanges();
+
+ MessageBox.Show($"Deleted {deletedCount} lines.", "Bulk Delete Complete",
+ MessageBoxButton.OK, MessageBoxImage.Information);
+ }
}
public class Animation : NotifyPropertyChangedBase
diff --git a/LegendaryExplorer/LegendaryExplorer/UserControls/ExportLoaderControls/InterpDataTimeline/Timeline.xaml b/LegendaryExplorer/LegendaryExplorer/UserControls/ExportLoaderControls/InterpDataTimeline/Timeline.xaml
index 57dec63871..d869ef6c20 100644
--- a/LegendaryExplorer/LegendaryExplorer/UserControls/ExportLoaderControls/InterpDataTimeline/Timeline.xaml
+++ b/LegendaryExplorer/LegendaryExplorer/UserControls/ExportLoaderControls/InterpDataTimeline/Timeline.xaml
@@ -1,4 +1,4 @@
-
- #FFC5CBF9
- #FFDDDDDD
- #FF000000
+ #FF264F78
+ #FF3D3D40
+
-
+
-
-
+
+
-
+
@@ -370,8 +364,8 @@
-
diff --git a/LegendaryExplorer/LegendaryExplorer/UserControls/ExportLoaderControls/InterpreterExportLoader.xaml b/LegendaryExplorer/LegendaryExplorer/UserControls/ExportLoaderControls/InterpreterExportLoader.xaml
index 07f651340b..6f3dc885a2 100644
--- a/LegendaryExplorer/LegendaryExplorer/UserControls/ExportLoaderControls/InterpreterExportLoader.xaml
+++ b/LegendaryExplorer/LegendaryExplorer/UserControls/ExportLoaderControls/InterpreterExportLoader.xaml
@@ -1,4 +1,4 @@
-
-
-
+
+
-
+
-
+
+ VerticalAlignment="Center" Foreground="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}">
+ VerticalAlignment="Center" Foreground="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}">
+ Command="{Binding MoveArrayElementUpCommand}" Foreground="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}">
+ Command="{Binding MoveArrayElementDownCommand}" Foreground="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}">
-
+
+ IsEnabled="{Binding CurrentLoadedExport, Converter={StaticResource NullEnabledConverter}}" Margin="0" ToolBar.OverflowMode="Never"
+ Style="{DynamicResource {x:Static ToolBar.TextBoxStyleKey}}"/>
+ KeyDown="ValueTextBox_KeyDown" ToolBar.OverflowMode="Never"
+ Style="{DynamicResource {x:Static ToolBar.ComboBoxStyleKey}}">
-
+
@@ -113,7 +115,8 @@
+ IsEnabled="{Binding CurrentLoadedExport, Converter={StaticResource NullEnabledConverter}}" ToolBar.OverflowMode="Never"
+ Style="{DynamicResource {x:Static ToolBar.TextBoxStyleKey}}"/>
@@ -290,13 +293,13 @@
+ Color="#0078D4" />
+ Color="White" />
+ Color="#505050" />
+ Color="White" />
diff --git a/LegendaryExplorer/LegendaryExplorer/UserControls/SharedToolControls/CurveGraph/CurveGraph.xaml.cs b/LegendaryExplorer/LegendaryExplorer/UserControls/SharedToolControls/CurveGraph/CurveGraph.xaml.cs
index d70cf64605..688400bbaf 100644
--- a/LegendaryExplorer/LegendaryExplorer/UserControls/SharedToolControls/CurveGraph/CurveGraph.xaml.cs
+++ b/LegendaryExplorer/LegendaryExplorer/UserControls/SharedToolControls/CurveGraph/CurveGraph.xaml.cs
@@ -13,6 +13,7 @@
using LegendaryExplorerCore.Gammtek.Extensions.Collections.Generic;
using LegendaryExplorerCore.Unreal;
using BezierSegment = LegendaryExplorer.UserControls.SharedToolControls.Curves.BezierSegment;
+using MessageBox = Xceed.Wpf.Toolkit.MessageBox;
namespace LegendaryExplorer.UserControls.SharedToolControls
{
diff --git a/LegendaryExplorer/LegendaryExplorer/UserControls/SharedToolControls/RecentsControl.xaml b/LegendaryExplorer/LegendaryExplorer/UserControls/SharedToolControls/RecentsControl.xaml
index ff75624190..27e26befe9 100644
--- a/LegendaryExplorer/LegendaryExplorer/UserControls/SharedToolControls/RecentsControl.xaml
+++ b/LegendaryExplorer/LegendaryExplorer/UserControls/SharedToolControls/RecentsControl.xaml
@@ -1,4 +1,4 @@
-
-
+
diff --git a/LegendaryExplorer/LegendaryExplorer/UserControls/SharedToolControls/Scene3D/MeshRenderContext.cs b/LegendaryExplorer/LegendaryExplorer/UserControls/SharedToolControls/Scene3D/MeshRenderContext.cs
index 6a819abaff..382a055c69 100644
--- a/LegendaryExplorer/LegendaryExplorer/UserControls/SharedToolControls/Scene3D/MeshRenderContext.cs
+++ b/LegendaryExplorer/LegendaryExplorer/UserControls/SharedToolControls/Scene3D/MeshRenderContext.cs
@@ -18,6 +18,7 @@
using Texture2D = SharpDX.Direct3D11.Texture2D;
using D2D = SharpDX.Direct2D1;
using DW = SharpDX.DirectWrite;
+using MessageBox = Xceed.Wpf.Toolkit.MessageBox;
namespace LegendaryExplorer.UserControls.SharedToolControls.Scene3D;
diff --git a/LegendaryExplorer/LegendaryExplorer/UserControls/SharedToolControls/Scene3D/SceneControlOptionsControl.xaml b/LegendaryExplorer/LegendaryExplorer/UserControls/SharedToolControls/Scene3D/SceneControlOptionsControl.xaml
index 3f05d3b017..85275994f5 100644
--- a/LegendaryExplorer/LegendaryExplorer/UserControls/SharedToolControls/Scene3D/SceneControlOptionsControl.xaml
+++ b/LegendaryExplorer/LegendaryExplorer/UserControls/SharedToolControls/Scene3D/SceneControlOptionsControl.xaml
@@ -1,14 +1,14 @@
-
-
+
+
diff --git a/LegendaryExplorer/LegendaryExplorer/UserControls/SharedToolControls/StatusBarGameIDIndicator.xaml b/LegendaryExplorer/LegendaryExplorer/UserControls/SharedToolControls/StatusBarGameIDIndicator.xaml
index a73aac433c..b4f4798d7d 100644
--- a/LegendaryExplorer/LegendaryExplorer/UserControls/SharedToolControls/StatusBarGameIDIndicator.xaml
+++ b/LegendaryExplorer/LegendaryExplorer/UserControls/SharedToolControls/StatusBarGameIDIndicator.xaml
@@ -1,4 +1,4 @@
-
+ /// A custom-drawn VScrollBar that supports dark mode theming.
+ ///
+ public class DarkScrollBar : VScrollBar
+ {
+ private bool _isDarkMode;
+ private Color _trackColor = Color.FromArgb(0x3E, 0x3E, 0x42); // Dark track
+ private Color _thumbColor = Color.FromArgb(0x68, 0x68, 0x6B); // Dark thumb
+ private Color _thumbHoverColor = Color.FromArgb(0x9E, 0x9E, 0x9E); // Lighter on hover
+ private Color _arrowColor = Color.FromArgb(0x99, 0x99, 0x99); // Arrow color
+ private Color _borderColor = Color.FromArgb(0x3F, 0x3F, 0x46); // Border
+
+ private bool _thumbHovered;
+ private bool _upArrowHovered;
+ private bool _downArrowHovered;
+
+ public DarkScrollBar()
+ {
+ SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer, true);
+ }
+
+ ///
+ /// Gets or sets whether dark mode is enabled for this scrollbar.
+ ///
+ public bool IsDarkMode
+ {
+ get => _isDarkMode;
+ set
+ {
+ if (_isDarkMode != value)
+ {
+ _isDarkMode = value;
+ // Toggle custom drawing based on dark mode
+ SetStyle(ControlStyles.UserPaint, value);
+ Invalidate();
+ }
+ }
+ }
+
+ ///
+ /// Gets or sets the track (background) color in dark mode.
+ ///
+ public Color TrackColor
+ {
+ get => _trackColor;
+ set { _trackColor = value; Invalidate(); }
+ }
+
+ ///
+ /// Gets or sets the thumb color in dark mode.
+ ///
+ public Color ThumbColor
+ {
+ get => _thumbColor;
+ set { _thumbColor = value; Invalidate(); }
+ }
+
+ ///
+ /// Gets or sets the thumb hover color in dark mode.
+ ///
+ public Color ThumbHoverColor
+ {
+ get => _thumbHoverColor;
+ set { _thumbHoverColor = value; Invalidate(); }
+ }
+
+ ///
+ /// Gets or sets the arrow button color in dark mode.
+ ///
+ public Color ArrowColor
+ {
+ get => _arrowColor;
+ set { _arrowColor = value; Invalidate(); }
+ }
+
+ protected override void OnPaint(PaintEventArgs e)
+ {
+ if (!_isDarkMode)
+ {
+ base.OnPaint(e);
+ return;
+ }
+
+ Graphics g = e.Graphics;
+ Rectangle rect = ClientRectangle;
+
+ // Draw track background
+ using (var trackBrush = new SolidBrush(_trackColor))
+ {
+ g.FillRectangle(trackBrush, rect);
+ }
+
+ // Draw border
+ using (var borderPen = new Pen(_borderColor))
+ {
+ g.DrawRectangle(borderPen, 0, 0, rect.Width - 1, rect.Height - 1);
+ }
+
+ int arrowHeight = SystemInformation.VerticalScrollBarArrowHeight;
+
+ // Draw up arrow button
+ Rectangle upArrowRect = new Rectangle(0, 0, rect.Width, arrowHeight);
+ DrawArrowButton(g, upArrowRect, true, _upArrowHovered);
+
+ // Draw down arrow button
+ Rectangle downArrowRect = new Rectangle(0, rect.Height - arrowHeight, rect.Width, arrowHeight);
+ DrawArrowButton(g, downArrowRect, false, _downArrowHovered);
+
+ // Calculate and draw thumb
+ if (Maximum > Minimum)
+ {
+ Rectangle thumbRect = GetThumbRect();
+ if (thumbRect.Height > 0)
+ {
+ Color currentThumbColor = _thumbHovered ? _thumbHoverColor : _thumbColor;
+ using (var thumbBrush = new SolidBrush(currentThumbColor))
+ {
+ // Draw rounded thumb
+ int padding = 2;
+ Rectangle innerThumb = new Rectangle(
+ thumbRect.X + padding,
+ thumbRect.Y + padding,
+ thumbRect.Width - (padding * 2),
+ thumbRect.Height - (padding * 2));
+
+ if (innerThumb.Width > 0 && innerThumb.Height > 0)
+ {
+ g.FillRectangle(thumbBrush, innerThumb);
+ }
+ }
+ }
+ }
+ }
+
+ private void DrawArrowButton(Graphics g, Rectangle rect, bool isUp, bool isHovered)
+ {
+ // Draw button background if hovered
+ if (isHovered)
+ {
+ using (var hoverBrush = new SolidBrush(Color.FromArgb(0x50, 0x50, 0x54)))
+ {
+ g.FillRectangle(hoverBrush, rect);
+ }
+ }
+
+ // Draw arrow
+ int arrowSize = 5;
+ int centerX = rect.X + rect.Width / 2;
+ int centerY = rect.Y + rect.Height / 2;
+
+ Point[] arrowPoints;
+ if (isUp)
+ {
+ arrowPoints = new Point[]
+ {
+ new Point(centerX, centerY - arrowSize / 2),
+ new Point(centerX - arrowSize, centerY + arrowSize / 2),
+ new Point(centerX + arrowSize, centerY + arrowSize / 2)
+ };
+ }
+ else
+ {
+ arrowPoints = new Point[]
+ {
+ new Point(centerX, centerY + arrowSize / 2),
+ new Point(centerX - arrowSize, centerY - arrowSize / 2),
+ new Point(centerX + arrowSize, centerY - arrowSize / 2)
+ };
+ }
+
+ using (var arrowBrush = new SolidBrush(_arrowColor))
+ {
+ g.FillPolygon(arrowBrush, arrowPoints);
+ }
+ }
+
+ private Rectangle GetThumbRect()
+ {
+ int arrowHeight = SystemInformation.VerticalScrollBarArrowHeight;
+ int trackHeight = Height - (arrowHeight * 2);
+
+ if (trackHeight <= 0 || Maximum <= Minimum)
+ return Rectangle.Empty;
+
+ int range = Maximum - Minimum;
+ int thumbHeight = Math.Max(20, (int)((float)LargeChange / (range + LargeChange) * trackHeight));
+
+ int availableTrack = trackHeight - thumbHeight;
+ int thumbPosition = (int)((float)(Value - Minimum) / range * availableTrack);
+
+ return new Rectangle(0, arrowHeight + thumbPosition, Width, thumbHeight);
+ }
+
+ protected override void OnMouseMove(MouseEventArgs e)
+ {
+ base.OnMouseMove(e);
+
+ if (!_isDarkMode) return;
+
+ int arrowHeight = SystemInformation.VerticalScrollBarArrowHeight;
+ Rectangle thumbRect = GetThumbRect();
+
+ bool wasThumbHovered = _thumbHovered;
+ bool wasUpHovered = _upArrowHovered;
+ bool wasDownHovered = _downArrowHovered;
+
+ _thumbHovered = thumbRect.Contains(e.Location);
+ _upArrowHovered = e.Y < arrowHeight;
+ _downArrowHovered = e.Y > Height - arrowHeight;
+
+ if (wasThumbHovered != _thumbHovered || wasUpHovered != _upArrowHovered || wasDownHovered != _downArrowHovered)
+ {
+ Invalidate();
+ }
+ }
+
+ protected override void OnMouseLeave(EventArgs e)
+ {
+ base.OnMouseLeave(e);
+
+ if (_isDarkMode)
+ {
+ _thumbHovered = false;
+ _upArrowHovered = false;
+ _downArrowHovered = false;
+ Invalidate();
+ }
+ }
+ }
+}
diff --git a/LegendaryExplorer/TexConverter/TexConverter.vcxproj b/LegendaryExplorer/TexConverter/TexConverter.vcxproj
index 5f32cea1dc..197207695e 100644
--- a/LegendaryExplorer/TexConverter/TexConverter.vcxproj
+++ b/LegendaryExplorer/TexConverter/TexConverter.vcxproj
@@ -26,17 +26,17 @@
DynamicLibrary
Unicode
- v143
+ v145
DynamicLibrary
Unicode
- v143
+ v145
DynamicLibrary
Unicode
- v143
+ v145
diff --git a/LegendaryExplorer/Xceed.Wpf.Toolkit/BusyIndicator/Themes/Generic.xaml b/LegendaryExplorer/Xceed.Wpf.Toolkit/BusyIndicator/Themes/Generic.xaml
index 30934d0579..ef56507e37 100644
--- a/LegendaryExplorer/Xceed.Wpf.Toolkit/BusyIndicator/Themes/Generic.xaml
+++ b/LegendaryExplorer/Xceed.Wpf.Toolkit/BusyIndicator/Themes/Generic.xaml
@@ -36,7 +36,7 @@
@@ -156,33 +156,12 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/LegendaryExplorer/Xceed.Wpf.Toolkit/ButtonSpinner/Themes/Generic.xaml b/LegendaryExplorer/Xceed.Wpf.Toolkit/ButtonSpinner/Themes/Generic.xaml
index 200efd1506..d9a6696cba 100644
--- a/LegendaryExplorer/Xceed.Wpf.Toolkit/ButtonSpinner/Themes/Generic.xaml
+++ b/LegendaryExplorer/Xceed.Wpf.Toolkit/ButtonSpinner/Themes/Generic.xaml
@@ -31,9 +31,9 @@
-
-
-
-
-
-
-
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:local="clr-namespace:Xceed.Wpf.Toolkit">
+
+
+
+
+
+
+
+
+
+ /// A custom-drawn VScrollBar that supports dark mode theming.
+ ///
+ public class DarkScrollBar : VScrollBar
+ {
+ private bool _isDarkMode;
+ private Color _trackColor = Color.FromArgb(0x3E, 0x3E, 0x42); // Dark track
+ private Color _thumbColor = Color.FromArgb(0x68, 0x68, 0x6B); // Dark thumb
+ private Color _thumbHoverColor = Color.FromArgb(0x9E, 0x9E, 0x9E); // Lighter on hover
+ private Color _arrowColor = Color.FromArgb(0x99, 0x99, 0x99); // Arrow color
+ private Color _borderColor = Color.FromArgb(0x3F, 0x3F, 0x46); // Border
+
+ private bool _thumbHovered;
+ private bool _upArrowHovered;
+ private bool _downArrowHovered;
+ private bool _isDragging;
+
+ // Windows messages
+ private const int WM_PAINT = 0x000F;
+ private const int WM_ERASEBKGND = 0x0014;
+ private const int WM_NCPAINT = 0x0085;
+ private const int WM_PRINTCLIENT = 0x0318;
+ private const int WM_PRINT = 0x0317;
+ private const int WM_LBUTTONDOWN = 0x0201;
+ private const int WM_LBUTTONUP = 0x0202;
+ private const int WM_MOUSEMOVE = 0x0200;
+ private const int WM_CAPTURECHANGED = 0x0215;
+
+ // Timer for continuous repaint during drag
+ private Timer _repaintTimer;
+
+ // For double buffering
+ private Bitmap _backBuffer;
+
+ public DarkScrollBar()
+ {
+ // Don't use UserPaint style - it breaks scrollbar functionality
+ SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
+
+ // Setup repaint timer for smooth updates during drag
+ _repaintTimer = new Timer();
+ _repaintTimer.Interval = 16; // ~60fps
+ _repaintTimer.Tick += (s, e) =>
+ {
+ if (_isDarkMode && _isDragging)
+ {
+ PaintDarkScrollBar();
+ }
+ };
+ }
+
+ ///
+ /// Gets or sets whether dark mode is enabled for this scrollbar.
+ ///
+ public bool IsDarkMode
+ {
+ get => _isDarkMode;
+ set
+ {
+ if (_isDarkMode != value)
+ {
+ _isDarkMode = value;
+ Invalidate();
+ }
+ }
+ }
+
+ ///
+ /// Gets or sets the track (background) color in dark mode.
+ ///
+ public Color TrackColor
+ {
+ get => _trackColor;
+ set { _trackColor = value; if (_isDarkMode) Invalidate(); }
+ }
+
+ ///
+ /// Gets or sets the thumb color in dark mode.
+ ///
+ public Color ThumbColor
+ {
+ get => _thumbColor;
+ set { _thumbColor = value; if (_isDarkMode) Invalidate(); }
+ }
+
+ ///
+ /// Gets or sets the thumb hover color in dark mode.
+ ///
+ public Color ThumbHoverColor
+ {
+ get => _thumbHoverColor;
+ set { _thumbHoverColor = value; if (_isDarkMode) Invalidate(); }
+ }
+
+ ///
+ /// Gets or sets the arrow button color in dark mode.
+ ///
+ public Color ArrowColor
+ {
+ get => _arrowColor;
+ set { _arrowColor = value; if (_isDarkMode) Invalidate(); }
+ }
+
+ ///
+ /// Override WndProc to intercept paint messages and handle them ourselves in dark mode.
+ ///
+ protected override void WndProc(ref Message m)
+ {
+ if (_isDarkMode)
+ {
+ switch (m.Msg)
+ {
+ case WM_ERASEBKGND:
+ // Prevent background erase flicker
+ m.Result = (IntPtr)1;
+ return;
+
+ case WM_NCPAINT:
+ // Handle non-client paint
+ m.Result = IntPtr.Zero;
+ return;
+
+ case WM_PAINT:
+ // Let base handle the scroll logic first
+ base.WndProc(ref m);
+ // Then immediately paint over with our dark theme
+ PaintDarkScrollBar();
+ return;
+
+ case WM_PRINTCLIENT:
+ case WM_PRINT:
+ // Handle print messages for proper rendering
+ PaintDarkScrollBar();
+ m.Result = IntPtr.Zero;
+ return;
+
+ case WM_LBUTTONDOWN:
+ _isDragging = true;
+ _repaintTimer.Start();
+ base.WndProc(ref m);
+ PaintDarkScrollBar();
+ return;
+
+ case WM_LBUTTONUP:
+ _isDragging = false;
+ _repaintTimer.Stop();
+ base.WndProc(ref m);
+ PaintDarkScrollBar();
+ return;
+
+ case WM_CAPTURECHANGED:
+ _isDragging = false;
+ _repaintTimer.Stop();
+ base.WndProc(ref m);
+ PaintDarkScrollBar();
+ return;
+
+ case WM_MOUSEMOVE:
+ base.WndProc(ref m);
+ if (_isDragging)
+ {
+ PaintDarkScrollBar();
+ }
+ return;
+ }
+ }
+
+ base.WndProc(ref m);
+ }
+
+ [DllImport("user32.dll")]
+ private static extern bool ValidateRect(IntPtr hWnd, IntPtr lpRect);
+
+ ///
+ /// Paints the scrollbar with dark mode colors using double buffering.
+ ///
+ private void PaintDarkScrollBar()
+ {
+ if (Width <= 0 || Height <= 0 || !IsHandleCreated || IsDisposed)
+ return;
+
+ try
+ {
+ // Create or resize back buffer
+ if (_backBuffer == null || _backBuffer.Width != Width || _backBuffer.Height != Height)
+ {
+ _backBuffer?.Dispose();
+ _backBuffer = new Bitmap(Width, Height);
+ }
+
+ // Draw to back buffer
+ using (Graphics bufferGraphics = Graphics.FromImage(_backBuffer))
+ {
+ Rectangle rect = ClientRectangle;
+
+ // Draw track background
+ using (var trackBrush = new SolidBrush(_trackColor))
+ {
+ bufferGraphics.FillRectangle(trackBrush, rect);
+ }
+
+ // Draw border
+ using (var borderPen = new Pen(_borderColor))
+ {
+ bufferGraphics.DrawRectangle(borderPen, 0, 0, rect.Width - 1, rect.Height - 1);
+ }
+
+ int arrowHeight = SystemInformation.VerticalScrollBarArrowHeight;
+
+ // Draw up arrow button
+ Rectangle upArrowRect = new Rectangle(0, 0, rect.Width, arrowHeight);
+ DrawArrowButton(bufferGraphics, upArrowRect, true, _upArrowHovered);
+
+ // Draw down arrow button
+ Rectangle downArrowRect = new Rectangle(0, rect.Height - arrowHeight, rect.Width, arrowHeight);
+ DrawArrowButton(bufferGraphics, downArrowRect, false, _downArrowHovered);
+
+ // Calculate and draw thumb
+ if (Maximum > Minimum)
+ {
+ Rectangle thumbRect = GetThumbRect();
+ if (thumbRect.Height > 0)
+ {
+ // Use hover color when dragging or hovering
+ Color currentThumbColor = (_thumbHovered || _isDragging) ? _thumbHoverColor : _thumbColor;
+ using (var thumbBrush = new SolidBrush(currentThumbColor))
+ {
+ // Draw thumb with padding
+ int padding = 2;
+ Rectangle innerThumb = new Rectangle(
+ thumbRect.X + padding,
+ thumbRect.Y + padding,
+ thumbRect.Width - (padding * 2),
+ thumbRect.Height - (padding * 2));
+
+ if (innerThumb.Width > 0 && innerThumb.Height > 0)
+ {
+ bufferGraphics.FillRectangle(thumbBrush, innerThumb);
+ }
+ }
+ }
+ }
+ }
+
+ // Copy back buffer to screen
+ using (Graphics screenGraphics = Graphics.FromHwnd(Handle))
+ {
+ screenGraphics.DrawImageUnscaled(_backBuffer, 0, 0);
+ }
+ }
+ catch
+ {
+ // Ignore paint errors (control might be disposing)
+ }
+ }
+
+ private void DrawArrowButton(Graphics g, Rectangle rect, bool isUp, bool isHovered)
+ {
+ // Draw button background if hovered
+ if (isHovered)
+ {
+ using (var hoverBrush = new SolidBrush(Color.FromArgb(0x50, 0x50, 0x54)))
+ {
+ g.FillRectangle(hoverBrush, rect);
+ }
+ }
+
+ // Draw arrow
+ int arrowSize = 5;
+ int centerX = rect.X + rect.Width / 2;
+ int centerY = rect.Y + rect.Height / 2;
+
+ Point[] arrowPoints;
+ if (isUp)
+ {
+ arrowPoints = new Point[]
+ {
+ new Point(centerX, centerY - arrowSize / 2),
+ new Point(centerX - arrowSize, centerY + arrowSize / 2),
+ new Point(centerX + arrowSize, centerY + arrowSize / 2)
+ };
+ }
+ else
+ {
+ arrowPoints = new Point[]
+ {
+ new Point(centerX, centerY + arrowSize / 2),
+ new Point(centerX - arrowSize, centerY - arrowSize / 2),
+ new Point(centerX + arrowSize, centerY - arrowSize / 2)
+ };
+ }
+
+ using (var arrowBrush = new SolidBrush(_arrowColor))
+ {
+ g.FillPolygon(arrowBrush, arrowPoints);
+ }
+ }
+
+ private Rectangle GetThumbRect()
+ {
+ int arrowHeight = SystemInformation.VerticalScrollBarArrowHeight;
+ int trackHeight = Height - (arrowHeight * 2);
+
+ if (trackHeight <= 0 || Maximum <= Minimum)
+ return Rectangle.Empty;
+
+ int range = Maximum - Minimum;
+ int thumbHeight = Math.Max(20, (int)((float)LargeChange / (range + LargeChange) * trackHeight));
+
+ int availableTrack = trackHeight - thumbHeight;
+ int thumbPosition = range > 0 ? (int)((float)(Value - Minimum) / range * availableTrack) : 0;
+
+ return new Rectangle(0, arrowHeight + thumbPosition, Width, thumbHeight);
+ }
+
+ protected override void OnMouseMove(MouseEventArgs e)
+ {
+ base.OnMouseMove(e);
+
+ if (!_isDarkMode) return;
+
+ int arrowHeight = SystemInformation.VerticalScrollBarArrowHeight;
+ Rectangle thumbRect = GetThumbRect();
+
+ bool wasThumbHovered = _thumbHovered;
+ bool wasUpHovered = _upArrowHovered;
+ bool wasDownHovered = _downArrowHovered;
+
+ _thumbHovered = thumbRect.Contains(e.Location);
+ _upArrowHovered = e.Y < arrowHeight;
+ _downArrowHovered = e.Y > Height - arrowHeight;
+
+ if (wasThumbHovered != _thumbHovered || wasUpHovered != _upArrowHovered || wasDownHovered != _downArrowHovered)
+ {
+ PaintDarkScrollBar();
+ }
+ }
+
+ protected override void OnMouseLeave(EventArgs e)
+ {
+ base.OnMouseLeave(e);
+
+ if (_isDarkMode)
+ {
+ _thumbHovered = false;
+ _upArrowHovered = false;
+ _downArrowHovered = false;
+ PaintDarkScrollBar();
+ }
+ }
+
+ protected override void OnValueChanged(EventArgs e)
+ {
+ base.OnValueChanged(e);
+
+ if (_isDarkMode)
+ {
+ PaintDarkScrollBar();
+ }
+ }
+
+ protected override void OnScroll(ScrollEventArgs se)
+ {
+ base.OnScroll(se);
+
+ if (_isDarkMode)
+ {
+ PaintDarkScrollBar();
+ }
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _repaintTimer?.Stop();
+ _repaintTimer?.Dispose();
+ _repaintTimer = null;
+ _backBuffer?.Dispose();
+ _backBuffer = null;
+ }
+ base.Dispose(disposing);
+ }
+ }
+}
diff --git a/SharedProjects/Be.Windows.Forms.HexBox/HexBox.cs b/SharedProjects/Be.Windows.Forms.HexBox/HexBox.cs
index 362ce1b41b..0c9c698845 100644
--- a/SharedProjects/Be.Windows.Forms.HexBox/HexBox.cs
+++ b/SharedProjects/Be.Windows.Forms.HexBox/HexBox.cs
@@ -1168,9 +1168,9 @@ protected override BytePositionInfo GetBytePositionInfo(Point p)
///
long _scrollVpos;
///
- /// Contains a vertical scroll
+ /// Contains a vertical scroll bar with dark mode support
///
- VScrollBar _vScrollBar;
+ DarkScrollBar _vScrollBar;
///
/// Contains a timer for thumbtrack scrolling
///
@@ -1480,7 +1480,7 @@ public bool Equals(HighlightRegion other)
public HexBox()
{
this.MaxBytesPerLine = 0x64; //default absolute max.
- this._vScrollBar = new VScrollBar();
+ this._vScrollBar = new DarkScrollBar();
this._vScrollBar.Scroll += _vScrollBar_Scroll;
this._builtInContextMenu = new BuiltInContextMenu(this);
@@ -2521,11 +2521,15 @@ public void HighlightSelected()
/// A PaintEventArgs that contains the event data.
protected override void OnPaintBackground(PaintEventArgs e)
{
+ // Check if using a custom/dark theme color (significantly darker than white)
+ // Brightness ranges from 0 (black) to 1 (white)
+ bool isCustomTheme = this.BackColor.GetBrightness() < 0.5f;
+
switch (_borderStyle)
{
case BorderStyle.Fixed3D:
{
- if (TextBoxRenderer.IsSupported)
+ if (TextBoxRenderer.IsSupported && !isCustomTheme)
{
VisualStyleElement state = VisualStyleElement.TextBox.TextEdit.Normal;
Color backColor = this.BackColor;
@@ -2551,11 +2555,9 @@ protected override void OnPaintBackground(PaintEventArgs e)
}
else
{
- // draw background
- e.Graphics.FillRectangle(new SolidBrush(BackColor), ClientRectangle);
-
- // draw default border
- ControlPaint.DrawBorder3D(e.Graphics, ClientRectangle, Border3DStyle.Sunken);
+ // Custom theme - skip visual styles and draw simple background
+ e.Graphics.FillRectangle(new SolidBrush(this.BackColor), this.ClientRectangle);
+ ControlPaint.DrawBorder3D(e.Graphics, this.ClientRectangle, Border3DStyle.Sunken);
}
break;
@@ -2563,16 +2565,16 @@ protected override void OnPaintBackground(PaintEventArgs e)
case BorderStyle.FixedSingle:
{
// draw background
- e.Graphics.FillRectangle(new SolidBrush(BackColor), ClientRectangle);
+ e.Graphics.FillRectangle(new SolidBrush(this.BackColor), this.ClientRectangle);
// draw fixed single border
- ControlPaint.DrawBorder(e.Graphics, ClientRectangle, Color.Black, ButtonBorderStyle.Solid);
+ ControlPaint.DrawBorder(e.Graphics, this.ClientRectangle, Color.Black, ButtonBorderStyle.Solid);
break;
}
default:
{
// draw background
- e.Graphics.FillRectangle(new SolidBrush(BackColor), ClientRectangle);
+ e.Graphics.FillRectangle(new SolidBrush(this.BackColor), this.ClientRectangle);
break;
}
}
@@ -2584,11 +2586,15 @@ protected override void OnPaintBackground(PaintEventArgs e)
/// A PaintEventArgs that contains the event data.
protected override void OnPaint(PaintEventArgs e)
{
- base.OnPaint(e);
-
if (_byteProvider == null)
return;
+ // Fill the entire visible area with the background color BEFORE any painting
+ // This ensures dark theme background is applied first
+ e.Graphics.Clear(this.BackColor);
+
+ base.OnPaint(e);
+
//System.Diagnostics.Debug.WriteLine("OnPaint " + DateTime.Now.ToString(), "HexBox");
// draw only in the content rectangle, so exclude the border and the scrollbar.
@@ -3370,6 +3376,29 @@ public bool VScrollBarVisible
}
bool _vScrollBarVisible;
+ ///
+ /// Gets the vertical scroll bar control.
+ /// This allows external code to customize scrollbar appearance for theming purposes.
+ ///
+ [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
+ public DarkScrollBar VScrollBar => _vScrollBar;
+
+ ///
+ /// Gets or sets whether the scrollbar uses dark mode rendering.
+ ///
+ [DefaultValue(false), Category("Appearance"), Description("Gets or sets whether the scrollbar uses dark mode rendering.")]
+ public bool ScrollBarDarkMode
+ {
+ get => _vScrollBar?.IsDarkMode ?? false;
+ set
+ {
+ if (_vScrollBar != null)
+ {
+ _vScrollBar.IsDarkMode = value;
+ }
+ }
+ }
+
///
/// Gets or sets the ByteProvider.
///
@@ -4340,17 +4369,28 @@ protected override void ScaleControl(SizeF factor, BoundsSpecified specified)
#region DarkModeSupport
+ // Instance-level color fields for per-control theming
+ private Color _instanceBackColor = Color.Empty;
+ private Color _instanceForeColor = Color.Empty;
+
///
- /// Gets the background color for the control.
+ /// Gets or sets the background color for the control.
///
- //[DefaultValue(typeof(Color), Control.D)]
- public override Color BackColor => backColor;
+ public override Color BackColor
+ {
+ get => _instanceBackColor != Color.Empty ? _instanceBackColor : backColor;
+ set { _instanceBackColor = value; Invalidate(); }
+ }
///
- /// Gets the background color for the control.
+ /// Gets or sets the foreground color for the control.
///
[DefaultValue(typeof(Color), "Black")]
- public override Color ForeColor => foreColor;
+ public override Color ForeColor
+ {
+ get => _instanceForeColor != Color.Empty ? _instanceForeColor : foreColor;
+ set { _instanceForeColor = value; Invalidate(); }
+ }
public static void SetColors(Color background, Color text)
{