Skip to content

Commit 49bc6f9

Browse files
authored
Merge pull request #5 from PawlygonStudio/feature/1.7.1
Release 1.7.1: Blendshape Fixes for VRCFT 1.x
2 parents 57d7a55 + f12c520 commit 49bc6f9

15 files changed

Lines changed: 1024 additions & 7 deletions

‎Packages/net.pawlygon.unitytools/Editor/AvatarSetupWizard.Finish.cs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ private static readonly (string Name, string Description, Action Open)[] NextSte
1717
("Getting Started", "Overview of every Pawlygon tool and when to use it.", GettingStarted.ShowWindow),
1818
("Eye Muscle Settings", "Set how far the eyes can look in each direction, with a live preview.", EyeMuscleSettings.ShowWindow),
1919
("Face Tracking Extras", "Ears, tail and pupils that react to your face.", FaceTrackingExtras.ShowWindow),
20+
("Blendshape Fixes", "Drive fix shapes like MouthSmileLeft-EyeClosedLeft from face tracking.", BlendshapeFixes.ShowWindow),
2021
("Patch Config Package Rules", "Add the packages (and minimum versions) your customers need to the patch configs.", PatchConfigRulesEditor.ShowWindow),
2122
};
2223

Lines changed: 345 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,345 @@
1+
using System;
2+
using System.Linq;
3+
using UnityEditor;
4+
using UnityEngine;
5+
6+
namespace Pawlygon.UnityTools.Editor
7+
{
8+
/// <summary>
9+
/// Editor window for Blendshape Fixes: finds the fix shapes on an avatar (helper blendshapes that prevent clipping
10+
/// while two face tracking shapes are active together, like MouthSmileLeft-EyeClosedLeft) and generates an
11+
/// animator that drives them from Pawlygon VRCFT 1.x face tracking, as a VRCFury prefab. The plan comes from
12+
/// <see cref="FTFixesPlan"/>, the animator from <see cref="FTFixesGenerator"/>.
13+
/// </summary>
14+
public class BlendshapeFixes : EditorWindow
15+
{
16+
private const string MenuPath = "!Pawlygon/Tools/Blendshape Fixes";
17+
private const string WindowTitle = "Blendshape Fixes";
18+
private const float SectionSpacing = 10f;
19+
20+
[SerializeField] private Vector2 scrollPosition;
21+
private GameObject selectedAvatar;
22+
private FTFixesProfile profile;
23+
private SerializedObject profileObject;
24+
private FTFixesPlan plan = new FTFixesPlan();
25+
private bool planDirty = true;
26+
private bool showParameters;
27+
private bool? prefabOnAvatarCache;
28+
private FTFixesGenerator.Result lastResult;
29+
private readonly PawlygonStatus status = new PawlygonStatus();
30+
31+
[MenuItem(MenuPath, priority = 42)] // Tools: Tune
32+
public static void ShowWindow()
33+
{
34+
var window = GetWindow<BlendshapeFixes>();
35+
window.titleContent = new GUIContent(WindowTitle);
36+
window.minSize = new Vector2(420f, 420f);
37+
}
38+
39+
private void OnEnable()
40+
{
41+
titleContent = new GUIContent(WindowTitle);
42+
Undo.undoRedoPerformed += OnUndoRedo;
43+
if (selectedAvatar == null) selectedAvatar = EyeMuscleSettingsCore.FindFirstAvatarInScene();
44+
LoadAvatar(selectedAvatar);
45+
}
46+
47+
private void OnDisable()
48+
{
49+
Undo.undoRedoPerformed -= OnUndoRedo;
50+
}
51+
52+
private void OnUndoRedo()
53+
{
54+
profileObject?.Update();
55+
MarkDirty();
56+
}
57+
58+
// Meshes, blendshapes or the generated prefab may have changed meanwhile
59+
private void OnHierarchyChange() => MarkDirty();
60+
private void OnProjectChange() => MarkDirty();
61+
private void OnFocus() => MarkDirty();
62+
63+
private void MarkDirty()
64+
{
65+
planDirty = true;
66+
prefabOnAvatarCache = null;
67+
Repaint();
68+
}
69+
70+
private void LoadAvatar(GameObject avatar)
71+
{
72+
selectedAvatar = avatar;
73+
lastResult = null;
74+
status.Clear();
75+
76+
profile = avatar != null ? FTFixesGenerator.FindProfile(avatar) : null;
77+
if (profile == null && avatar != null)
78+
{
79+
// Saved with the generated files on the first Generate
80+
profile = CreateInstance<FTFixesProfile>();
81+
profile.hideFlags = HideFlags.DontSave;
82+
}
83+
profileObject = profile != null ? new SerializedObject(profile) : null;
84+
MarkDirty();
85+
}
86+
87+
// =====================================================================
88+
// OnGUI
89+
// =====================================================================
90+
91+
private void OnGUI()
92+
{
93+
PawlygonEditorUI.EnsureStyles();
94+
95+
// Everything that decides which controls are drawn is refreshed before the layout pass.
96+
if (Event.current.type == EventType.Layout && planDirty)
97+
{
98+
planDirty = false;
99+
plan = FTFixesPlan.Resolve(profile, selectedAvatar);
100+
}
101+
102+
PawlygonEditorUI.DrawHeader(
103+
WindowTitle,
104+
"Drives fix blendshapes like MouthSmileLeft-EyeClosedLeft from VRCFT face tracking (Pawlygon VRCFT 1.x).",
105+
PawlygonEditorUI.DocumentationUrl);
106+
107+
if (PawlygonEditorUI.DrawAvatarBar(this, ref selectedAvatar, "Avatar"))
108+
{
109+
LoadAvatar(selectedAvatar);
110+
GUIUtility.ExitGUI();
111+
}
112+
EditorGUILayout.Space(6f);
113+
114+
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition, GUILayout.ExpandHeight(true));
115+
if (selectedAvatar == null || profileObject == null)
116+
{
117+
DrawNoAvatar();
118+
}
119+
else
120+
{
121+
profileObject.Update();
122+
EditorGUI.BeginChangeCheck();
123+
124+
DrawFixes();
125+
EditorGUILayout.Space(SectionSpacing);
126+
DrawManualFixes();
127+
EditorGUILayout.Space(SectionSpacing);
128+
DrawParameters();
129+
EditorGUILayout.Space(SectionSpacing);
130+
DrawGenerate();
131+
132+
if (EditorGUI.EndChangeCheck())
133+
{
134+
profileObject.ApplyModifiedProperties();
135+
planDirty = true;
136+
}
137+
}
138+
EditorGUILayout.EndScrollView();
139+
140+
PawlygonEditorUI.DrawStatusBar(status);
141+
if (selectedAvatar != null && profile != null) DrawActions();
142+
PawlygonEditorUI.DrawFooter();
143+
}
144+
145+
private void DrawNoAvatar()
146+
{
147+
using (new EditorGUILayout.VerticalScope(PawlygonEditorUI.SectionStyle))
148+
{
149+
PawlygonEditorUI.DrawSectionHeader(
150+
"Pick an Avatar",
151+
"Choose your avatar above: drag it from the Hierarchy, pick it from the Scene list, or select it and click Use Selection.");
152+
}
153+
}
154+
155+
private void DrawFixes()
156+
{
157+
using (new EditorGUILayout.VerticalScope(PawlygonEditorUI.SectionStyle))
158+
{
159+
PawlygonEditorUI.DrawSectionHeader(
160+
"Fixes",
161+
"Fix shapes are helper blendshapes that prevent clipping while two face tracking shapes are active together, " +
162+
"named after the shapes they fix and joined with '-' (e.g. MouthSmileLeft-EyeClosedLeft). Each one follows the " +
163+
"same VRCFT parameters as the shapes it fixes.");
164+
165+
EditorGUILayout.PropertyField(profileObject.FindProperty(nameof(FTFixesProfile.autoDetect)), new GUIContent("Auto-detect"));
166+
EditorGUILayout.PropertyField(profileObject.FindProperty(nameof(FTFixesProfile.combine)), new GUIContent("Combine"));
167+
EditorGUILayout.Space(4f);
168+
169+
var detected = plan.Entries.Where(e => !e.IsManual).ToList();
170+
if (!profile.autoDetect)
171+
{
172+
EditorGUILayout.LabelField("Auto-detect is off: only the manual fixes below are driven.", PawlygonEditorUI.SubLabelStyle);
173+
return;
174+
}
175+
if (detected.Count == 0)
176+
{
177+
EditorGUILayout.LabelField(
178+
"No fix shapes found: no blendshape on the avatar is named after face tracking shapes joined with '-'.",
179+
PawlygonEditorUI.SubLabelStyle);
180+
return;
181+
}
182+
183+
foreach (var entry in detected) DrawDetectedRow(entry);
184+
}
185+
}
186+
187+
private void DrawDetectedRow(FTFixesPlan.Entry entry)
188+
{
189+
using (new EditorGUILayout.HorizontalScope())
190+
{
191+
bool include = EditorGUILayout.ToggleLeft(new GUIContent(entry.Blendshape, "Drive this fix."), !entry.Excluded, GUILayout.MinWidth(200f));
192+
if (include == entry.Excluded)
193+
{
194+
Undo.RecordObject(profile, include ? "Include Blendshape Fix" : "Exclude Blendshape Fix");
195+
if (include) profile.excludedFixes.Remove(entry.Blendshape);
196+
else if (!profile.excludedFixes.Contains(entry.Blendshape)) profile.excludedFixes.Add(entry.Blendshape);
197+
EditorUtility.SetDirty(profile);
198+
profileObject.Update();
199+
planDirty = true;
200+
}
201+
202+
GUILayout.FlexibleSpace();
203+
if (entry.Excluded) PawlygonEditorUI.DrawBadge("Off", PawlygonEditorUI.BadgeKind.Neutral, "Not driven.");
204+
else PawlygonEditorUI.DrawBadge(Plural(entry.MeshPaths.Count, "mesh", "meshes"), PawlygonEditorUI.BadgeKind.Ok, string.Join("\n", entry.MeshPaths));
205+
}
206+
EditorGUILayout.LabelField(DescribeSources(entry), PawlygonEditorUI.RichMiniLabelStyle);
207+
}
208+
209+
private void DrawManualFixes()
210+
{
211+
using (new EditorGUILayout.VerticalScope(PawlygonEditorUI.SectionStyle))
212+
{
213+
PawlygonEditorUI.DrawSectionHeader(
214+
"Manual Fixes",
215+
"Fix shapes named differently: the fix blendshape and the shapes it fixes, separated by commas. " +
216+
"An entry with the same blendshape as a detected fix replaces it.");
217+
218+
EditorGUILayout.PropertyField(profileObject.FindProperty(nameof(FTFixesProfile.fixes)), new GUIContent("Fixes"), true);
219+
220+
foreach (var entry in plan.Entries.Where(e => e.IsManual))
221+
{
222+
if (entry.Problem != null) EditorGUILayout.HelpBox($"{entry.Blendshape}: {entry.Problem}", MessageType.Warning);
223+
else EditorGUILayout.LabelField($"<b>{entry.Blendshape}</b> {DescribeSources(entry)}", PawlygonEditorUI.RichMiniLabelStyle);
224+
}
225+
}
226+
}
227+
228+
private void DrawParameters()
229+
{
230+
using (new EditorGUILayout.VerticalScope(PawlygonEditorUI.SectionStyle))
231+
{
232+
showParameters = EditorGUILayout.Foldout(showParameters, "Parameters", true);
233+
if (!showParameters) return;
234+
235+
EditorGUILayout.LabelField(
236+
"The fixes read the same VRCFT parameters as the Pawlygon VRCFT 1.x animator: the synced OSCmooth proxies by default. " +
237+
"Fixes of eye shapes only run while eye tracking is on, fixes of mouth shapes while lip tracking is on.",
238+
PawlygonEditorUI.SubLabelStyle);
239+
EditorGUILayout.PropertyField(profileObject.FindProperty(nameof(FTFixesProfile.parameterPrefix)), new GUIContent("Parameter Prefix"));
240+
EditorGUILayout.PropertyField(profileObject.FindProperty(nameof(FTFixesProfile.eyeTrackingActive)), new GUIContent("Eye Tracking Active"));
241+
EditorGUILayout.PropertyField(profileObject.FindProperty(nameof(FTFixesProfile.lipTrackingActive)), new GUIContent("Lip Tracking Active"));
242+
}
243+
}
244+
245+
private void DrawGenerate()
246+
{
247+
using (new EditorGUILayout.VerticalScope(PawlygonEditorUI.SectionStyle))
248+
{
249+
PawlygonEditorUI.DrawSectionHeader(
250+
"Generate",
251+
"Builds an FX controller that drives the fixes and a VRCFury prefab to add to the avatar. Needs VRCFury and the Pawlygon VRCFT 1.x prefab on the avatar.");
252+
253+
int count = plan.Generated.Count();
254+
EditorGUILayout.LabelField($"<b>Fixes:</b> {count}", PawlygonEditorUI.RichMiniLabelStyle);
255+
EditorGUILayout.LabelField($"<b>Output:</b> {FTFixesGenerator.GetOutputFolder(selectedAvatar)}", PawlygonEditorUI.RichMiniLabelStyle);
256+
257+
if (!AssetDatabase.Contains(profile))
258+
{
259+
EditorGUILayout.LabelField("These settings are saved with the generated files.", PawlygonEditorUI.SubLabelStyle);
260+
}
261+
else if (!string.IsNullOrEmpty(profile.lastGeneratedSignature) && profile.lastGeneratedSignature != FTFixesPlan.Signature(profile, plan))
262+
{
263+
EditorGUILayout.Space(4f);
264+
EditorGUILayout.HelpBox("The fixes or settings changed since the last generation. Generate again to update the animator.", MessageType.Warning);
265+
}
266+
267+
if (lastResult != null)
268+
{
269+
foreach (string warning in lastResult.Warnings) EditorGUILayout.HelpBox(warning, MessageType.Warning);
270+
}
271+
}
272+
}
273+
274+
private void DrawActions()
275+
{
276+
bool hasPrefab = AssetDatabase.LoadAssetAtPath<GameObject>(FTFixesGenerator.GetPrefabPath(selectedAvatar)) != null;
277+
if (prefabOnAvatarCache == null) prefabOnAvatarCache = hasPrefab && FTFixesGenerator.IsPrefabOnAvatar(selectedAvatar);
278+
bool onAvatar = prefabOnAvatarCache.Value;
279+
280+
PawlygonEditorUI.BeginActionBar();
281+
282+
using (new EditorGUI.DisabledScope(!hasPrefab || onAvatar))
283+
{
284+
var addContent = new GUIContent(
285+
onAvatar ? "✓ Prefab Is on the Avatar" : "Add Prefab to Avatar",
286+
!hasPrefab ? "Generate first: this adds the generated prefab to the avatar." : $"Adds {FTFixesGenerator.PrefabName} to {selectedAvatar.name}.");
287+
if (PawlygonEditorUI.DrawSecondaryButton(addContent, 28f))
288+
{
289+
FTFixesGenerator.AddPrefabToAvatar(selectedAvatar);
290+
prefabOnAvatarCache = null;
291+
GUIUtility.ExitGUI();
292+
}
293+
}
294+
295+
GUILayout.FlexibleSpace();
296+
297+
using (new EditorGUI.DisabledScope(!plan.Generated.Any()))
298+
{
299+
if (PawlygonEditorUI.DrawPrimaryButton(new GUIContent("Generate", "Builds the FX controller and the VRCFury prefab."), 28f, GUILayout.Width(150f)))
300+
{
301+
Generate();
302+
GUIUtility.ExitGUI();
303+
}
304+
}
305+
306+
PawlygonEditorUI.EndActionBar();
307+
}
308+
309+
private void Generate()
310+
{
311+
try
312+
{
313+
bool wasSaved = AssetDatabase.Contains(profile);
314+
if (!wasSaved) profile.hideFlags = HideFlags.None;
315+
316+
lastResult = FTFixesGenerator.Generate(profile, selectedAvatar);
317+
if (!wasSaved && AssetDatabase.Contains(profile)) profileObject = new SerializedObject(profile);
318+
319+
if (!lastResult.Success) status.Error(lastResult.Message);
320+
else if (lastResult.PrefabPath != null)
321+
{
322+
status.Info(lastResult.Message, "Ping", PawlygonStatus.Ping(AssetDatabase.LoadAssetAtPath<GameObject>(lastResult.PrefabPath)));
323+
}
324+
else status.Warning(lastResult.Message);
325+
}
326+
catch (Exception ex)
327+
{
328+
Debug.LogException(ex);
329+
status.Error($"Generation failed: {ex.Message}");
330+
}
331+
finally
332+
{
333+
if (!AssetDatabase.Contains(profile)) profile.hideFlags = HideFlags.DontSave;
334+
MarkDirty();
335+
}
336+
}
337+
338+
private static string DescribeSources(FTFixesPlan.Entry entry)
339+
{
340+
return "Fixes " + string.Join(" + ", entry.Sources.Select(s => $"{s.Shape} <color=#888888>({s.Parameter})</color>"));
341+
}
342+
343+
private static string Plural(int count, string singular, string plural) => $"{count} {(count == 1 ? singular : plural)}";
344+
}
345+
}

‎Packages/net.pawlygon.unitytools/Editor/BlendshapeFixes.cs.meta‎

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)