├── .github └── FUNDING.yml ├── Assets ├── Hai.meta └── Hai │ ├── AnimationViewer.meta │ ├── AnimationViewer │ ├── Scripts.meta │ └── Scripts │ │ ├── Editor.meta │ │ └── Editor │ │ ├── AnimationViewerEditorWindow.cs │ │ ├── AnimationViewerEditorWindow.cs.meta │ │ ├── AnimationViewerGenerator.cs │ │ └── AnimationViewerGenerator.cs.meta │ ├── BlendshapeViewer.meta │ ├── BlendshapeViewer │ ├── Scripts.meta │ ├── Scripts │ │ ├── Editor.meta │ │ └── Editor │ │ │ ├── BlendshapeViewerDiffCompute.cs │ │ │ ├── BlendshapeViewerDiffCompute.cs.meta │ │ │ ├── BlendshapeViewerEditorWindow.cs │ │ │ ├── BlendshapeViewerEditorWindow.cs.meta │ │ │ ├── BlendshapeViewerGenerator.cs │ │ │ ├── BlendshapeViewerGenerator.cs.meta │ │ │ ├── DiffCompute.compute │ │ │ └── DiffCompute.compute.meta │ ├── Shaders.meta │ └── Shaders │ │ ├── HaiBlendshapeViewer.shader │ │ ├── HaiBlendshapeViewer.shader.meta │ │ ├── HaiBlendshapeViewerRectOnly.shader │ │ └── HaiBlendshapeViewerRectOnly.shader.meta │ ├── VisualExpressionsEditor.meta │ └── VisualExpressionsEditor │ ├── Scripts.meta │ ├── Scripts │ ├── Editor.meta │ └── Editor │ │ ├── VEEDiffCompute.compute │ │ ├── VEEDiffCompute.compute.meta │ │ ├── VisualExpressionsEditorDiffCompute.cs │ │ ├── VisualExpressionsEditorDiffCompute.cs.meta │ │ ├── VisualExpressionsEditorGeneratorClip.cs │ │ ├── VisualExpressionsEditorGeneratorClip.cs.meta │ │ ├── VisualExpressionsEditorGeneratorSingular.cs │ │ ├── VisualExpressionsEditorGeneratorSingular.cs.meta │ │ ├── VisualExpressionsEditorWindow.cs │ │ └── VisualExpressionsEditorWindow.cs.meta │ ├── Shaders.meta │ └── Shaders │ ├── HaiVisualExpressionsEditor.shader │ ├── HaiVisualExpressionsEditor.shader.meta │ ├── HaiVisualExpressionsEditorRectOnly.shader │ └── HaiVisualExpressionsEditorRectOnly.shader.meta ├── LICENSE └── README.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: vr_hai 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: ['https://hai-vr.fanbox.cc/'] 14 | -------------------------------------------------------------------------------- /Assets/Hai.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 6bc60172a3b798747b7bf15e5d32f335 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Assets/Hai/AnimationViewer.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 6b1e2876697f44d1ad9b4ce24f452c98 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Assets/Hai/AnimationViewer/Scripts.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 0c37422a161c443b89bd5f892d8f222a 3 | timeCreated: 1643687100 -------------------------------------------------------------------------------- /Assets/Hai/AnimationViewer/Scripts/Editor.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 6b26d65829d142df991b393fb9ca2bed 3 | timeCreated: 1643687105 -------------------------------------------------------------------------------- /Assets/Hai/AnimationViewer/Scripts/Editor/AnimationViewerEditorWindow.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using UnityEditor; 6 | using UnityEngine; 7 | using Object = UnityEngine.Object; 8 | 9 | namespace Hai.AnimationViewer.Scripts.Editor 10 | { 11 | [InitializeOnLoad] 12 | public class AnimationViewerEditorWindow : EditorWindow 13 | { 14 | public Animator animator; 15 | public bool autoUpdateOnFocus = true; 16 | public bool continuousUpdates; 17 | public int updateSpeed = 50; 18 | public bool advanced; 19 | public float normalizedTime; 20 | public HumanBodyBones focusedBone = HumanBodyBones.Head; 21 | public AnimationClip basePose; 22 | public int thumbnailSize = 96; 23 | public bool updateOnActivate = true; 24 | private Vector2 _scrollPos; 25 | // private Animator _generatedFor; 26 | private int _generatedSize; 27 | 28 | private Vector3 _generatedTransformPosition; 29 | private Quaternion _generatedTransformRotation; 30 | private float _generatedFieldOfView; 31 | private bool _generatedOrthographic; 32 | private float _generatedNearClipPlane; 33 | private float _generatedFarClipPlane; 34 | private float _generatedOrthographicSize; 35 | 36 | public AnimationViewerEditorWindow() 37 | { 38 | titleContent = new GUIContent("AnimationViewer"); 39 | EditorApplication.projectWindowItemOnGUI -= DrawAnimationClipItem; // Clear any previously added 40 | EditorApplication.projectWindowItemOnGUI += DrawAnimationClipItem; 41 | 42 | try 43 | { 44 | _projectBrowserType = typeof(EditorWindow).Assembly.GetType("UnityEditor.ProjectBrowser"); 45 | _projectBrowserListAreaField = _projectBrowserType.GetField("m_ListArea", BindingFlags.NonPublic | BindingFlags.Instance); 46 | 47 | var listAreaType = typeof(EditorWindow).Assembly.GetType("UnityEditor.ObjectListArea"); 48 | _listAreaGridSizeField = listAreaType.GetProperty("gridSize", BindingFlags.Public | BindingFlags.Instance); 49 | 50 | _thumbnailSizeFeatureAvailable = true; 51 | } 52 | catch (Exception e) 53 | { 54 | Debug.LogException(e); 55 | } 56 | } 57 | 58 | private void Update() 59 | { 60 | if (!_enabled) return; 61 | if (!autoUpdateOnFocus) return; 62 | if (animator == null) return; 63 | if (!HasGenerationParamsChanged()) return; 64 | 65 | Invalidate(); 66 | if (continuousUpdates) 67 | { 68 | EditorApplication.RepaintProjectWindow(); 69 | } 70 | } 71 | 72 | private void OnGUI() 73 | { 74 | _scrollPos = GUILayout.BeginScrollView(_scrollPos, GUILayout.Height(position.height - EditorGUIUtility.singleLineHeight)); 75 | var serializedObject = new SerializedObject(this); 76 | EditorGUILayout.PropertyField(serializedObject.FindProperty(nameof(animator))); 77 | EditorGUILayout.PropertyField(serializedObject.FindProperty(nameof(autoUpdateOnFocus))); 78 | EditorGUI.BeginDisabledGroup(!autoUpdateOnFocus); 79 | EditorGUILayout.PropertyField(serializedObject.FindProperty(nameof(continuousUpdates))); 80 | if (autoUpdateOnFocus && continuousUpdates) 81 | { 82 | EditorGUILayout.HelpBox("Continuous Updates will cause a dramatic slow down.\nDisable it when not in use.", MessageType.Warning); 83 | } 84 | EditorGUI.EndDisabledGroup(); 85 | EditorGUILayout.IntSlider(serializedObject.FindProperty(nameof(updateSpeed)), 1, 100); 86 | if (_thumbnailSizeFeatureAvailable) 87 | { 88 | var thumbnailSizeSerialized = serializedObject.FindProperty(nameof(thumbnailSize)); 89 | var previousSize = thumbnailSizeSerialized.intValue; 90 | EditorGUILayout.IntSlider(thumbnailSizeSerialized, 20, 300); 91 | if (_enabled) 92 | { 93 | var editorWindow = GetWindow(_projectBrowserType, false, null, false); 94 | var listArea = _projectBrowserListAreaField.GetValue(editorWindow); 95 | _listAreaGridSizeField.SetValue(listArea, thumbnailSize); 96 | if (previousSize != thumbnailSizeSerialized.intValue) 97 | { 98 | EditorApplication.RepaintProjectWindow(); 99 | } 100 | } 101 | } 102 | 103 | EditorGUI.BeginDisabledGroup(animator == null || AnimationMode.InAnimationMode()); 104 | if (ColoredBgButton(_enabled, Color.red, () => GUILayout.Button("Activate Viewer"))) 105 | { 106 | _enabled = !_enabled; 107 | if (_enabled && updateOnActivate) 108 | { 109 | Invalidate(); 110 | } 111 | EditorApplication.RepaintProjectWindow(); 112 | } 113 | if (_enabled) 114 | { 115 | EditorGUILayout.BeginHorizontal(); 116 | if (GUILayout.Button("Update")) 117 | { 118 | Invalidate(); 119 | EditorApplication.RepaintProjectWindow(); 120 | } 121 | if (GUILayout.Button("Force", GUILayout.Width(50))) 122 | { 123 | _projectRenderQueue.ForceClearAll(); 124 | EditorApplication.RepaintProjectWindow(); 125 | } 126 | EditorGUILayout.EndHorizontal(); 127 | } 128 | EditorGUI.EndDisabledGroup(); 129 | 130 | advanced = EditorGUILayout.Foldout(advanced, "Advanced"); 131 | if (advanced) 132 | { 133 | EditorGUILayout.PropertyField(serializedObject.FindProperty(nameof(focusedBone))); 134 | EditorGUILayout.Slider(serializedObject.FindProperty(nameof(normalizedTime)), 0f, 1f); 135 | EditorGUILayout.PropertyField(serializedObject.FindProperty(nameof(basePose))); 136 | EditorGUILayout.PropertyField(serializedObject.FindProperty(nameof(updateOnActivate))); 137 | } 138 | else 139 | { 140 | if (basePose != null) 141 | { 142 | EditorGUILayout.PropertyField(serializedObject.FindProperty(nameof(basePose))); 143 | } 144 | } 145 | 146 | if (basePose != null) 147 | { 148 | EditorGUILayout.HelpBox("A base pose is specified. This will change the way animations look.", MessageType.Warning); 149 | if (GUILayout.Button("Discard base pose")) 150 | { 151 | serializedObject.FindProperty(nameof(basePose)).objectReferenceValue = null; 152 | } 153 | } 154 | if (focusedBone != HumanBodyBones.Head) 155 | { 156 | if (GUILayout.Button("Reset focused bone to Head")) 157 | { 158 | serializedObject.FindProperty(nameof(focusedBone)).intValue = (int)HumanBodyBones.Head; 159 | } 160 | } 161 | serializedObject.ApplyModifiedProperties(); 162 | 163 | if (animator != null) 164 | { 165 | _focusedObjectNullable = animator.gameObject; 166 | _projectRenderQueue.QueueSize(updateSpeed); 167 | _projectRenderQueue.Bone(focusedBone); 168 | _projectRenderQueue.NormalizedTime(normalizedTime); 169 | _projectRenderQueue.BasePose(basePose); 170 | } 171 | EditorGUILayout.EndScrollView(); 172 | } 173 | 174 | private void SaveGenerationParams() 175 | { 176 | // _generatedFor = animator; 177 | 178 | var sceneCamera = SceneView.lastActiveSceneView.camera; 179 | _generatedTransformPosition = sceneCamera.transform.position; 180 | _generatedTransformRotation = sceneCamera.transform.rotation; 181 | var whRatio = (1f * sceneCamera.pixelWidth / sceneCamera.pixelHeight); 182 | _generatedFieldOfView = whRatio < 1 ? sceneCamera.fieldOfView * whRatio : sceneCamera.fieldOfView; 183 | _generatedOrthographic = sceneCamera.orthographic; 184 | _generatedNearClipPlane = sceneCamera.nearClipPlane; 185 | _generatedFarClipPlane = sceneCamera.farClipPlane; 186 | _generatedOrthographicSize = sceneCamera.orthographicSize; 187 | _generatedNormalizedTime = normalizedTime; 188 | } 189 | 190 | private bool HasGenerationParamsChanged() 191 | { 192 | var sceneCamera = SceneView.lastActiveSceneView.camera; 193 | if (_generatedTransformPosition != sceneCamera.transform.position) return true; 194 | if (_generatedTransformRotation != sceneCamera.transform.rotation) return true; 195 | var whRatio = (1f * sceneCamera.pixelWidth / sceneCamera.pixelHeight); 196 | if (Math.Abs(_generatedFieldOfView - (whRatio < 1 ? sceneCamera.fieldOfView * whRatio : sceneCamera.fieldOfView)) > 0.001f) return true; 197 | if (_generatedOrthographic != sceneCamera.orthographic) return true; 198 | if (Math.Abs(_generatedNearClipPlane - sceneCamera.nearClipPlane) > 0.001f) return true; 199 | if (Math.Abs(_generatedFarClipPlane - sceneCamera.farClipPlane) > 0.001f) return true; 200 | if (Math.Abs(_generatedOrthographicSize - sceneCamera.orthographicSize) > 0.001f) return true; 201 | if (Math.Abs(_generatedNormalizedTime - normalizedTime) > 0.001f) return true; 202 | return false; 203 | } 204 | 205 | private void UsingAnimator(Animator inAnimator) 206 | { 207 | animator = inAnimator; 208 | } 209 | 210 | private static bool ColoredBgButton(bool isActive, Color bgColor, Func inside) 211 | { 212 | var col = GUI.backgroundColor; 213 | try 214 | { 215 | if (isActive) GUI.backgroundColor = bgColor; 216 | return inside(); 217 | } 218 | finally 219 | { 220 | GUI.backgroundColor = col; 221 | } 222 | } 223 | 224 | [MenuItem("Window/Haï/AnimationViewer")] 225 | public static void ShowWindow() 226 | { 227 | Obtain().Show(); 228 | } 229 | 230 | [MenuItem("CONTEXT/Animator/Haï AnimationViewer")] 231 | public static void OpenEditor(MenuCommand command) 232 | { 233 | var window = Obtain(); 234 | window.UsingAnimator((Animator) command.context); 235 | window.Show(); 236 | _enabled = true; 237 | window.Invalidate(); 238 | EditorApplication.RepaintProjectWindow(); 239 | } 240 | 241 | private static AnimationViewerEditorWindow Obtain() 242 | { 243 | var editor = GetWindow(false, null, false); 244 | editor.titleContent = new GUIContent("AnimationViewer"); 245 | return editor; 246 | } 247 | 248 | private static readonly AnimationViewerRenderQueue _projectRenderQueue; 249 | private static bool _delayedThisFrame; 250 | private static GameObject _focusedObjectNullable; 251 | private static bool _enabled; 252 | private float _generatedNormalizedTime; 253 | private readonly bool _thumbnailSizeFeatureAvailable; 254 | private readonly FieldInfo _projectBrowserListAreaField; 255 | private readonly PropertyInfo _listAreaGridSizeField; 256 | private readonly Type _projectBrowserType; 257 | 258 | static AnimationViewerEditorWindow() 259 | { 260 | _projectRenderQueue = new AnimationViewerRenderQueue(); 261 | } 262 | 263 | private static void DrawAnimationClipItem(string guid, Rect selectionRect) 264 | { 265 | if (!_enabled) return; 266 | 267 | var assetPath = AssetDatabase.GUIDToAssetPath(guid); 268 | if (!assetPath.EndsWith(".anim")) return; 269 | 270 | var texture = _projectRenderQueue.RequireRender(assetPath); 271 | 272 | GUI.Box(selectionRect, texture); 273 | 274 | if (!_delayedThisFrame) 275 | { 276 | EditorApplication.delayCall += Rerender; 277 | _delayedThisFrame = true; 278 | } 279 | } 280 | 281 | private static void Rerender() 282 | { 283 | _delayedThisFrame = false; 284 | 285 | if (AnimationMode.InAnimationMode()) return; 286 | 287 | if (_focusedObjectNullable == null) return; 288 | 289 | var animator = _focusedObjectNullable.transform.GetComponentInParent(); 290 | if (animator == null) return; 291 | 292 | _projectRenderQueue.TryRender(animator.gameObject); 293 | } 294 | 295 | private void Invalidate() 296 | { 297 | _projectRenderQueue.ForceInvalidate(); 298 | SaveGenerationParams(); 299 | } 300 | } 301 | 302 | // Note: I tried to implement a UnityEditor.Editor for AnimationClip, through the "RenderStaticPreview" method, 303 | // but the project view would not display the icon, despite the inspector displaying the asset icon as expected. 304 | // Afterwards, I've settled with using the "projectWindowItemOnGUI" callback, as seen above. 305 | public class AnimationViewerRenderQueue 306 | { 307 | private readonly Dictionary _pathToTexture; 308 | private readonly List _invalidation; 309 | private Queue _queue; 310 | private int _queueSize; 311 | private HumanBodyBones _bone = HumanBodyBones.Head; 312 | private float _normalizedTime; 313 | private AnimationClip _basePose; 314 | 315 | public AnimationViewerRenderQueue() 316 | { 317 | _pathToTexture = new Dictionary(); 318 | _invalidation = new List(); 319 | _queue = new Queue(); 320 | } 321 | 322 | public void ForceClearAll() 323 | { 324 | _pathToTexture.Clear(); 325 | _queue.Clear(); 326 | _invalidation.Clear(); 327 | } 328 | 329 | public void ForceInvalidate() 330 | { 331 | _invalidation.AddRange(_pathToTexture.Keys); 332 | } 333 | 334 | public Texture2D RequireRender(string assetPath) 335 | { 336 | if (_pathToTexture.ContainsKey(assetPath) 337 | && _pathToTexture[assetPath] != null) // Can happen when the texture is destroyed (Unity invalid object) 338 | { 339 | if (!_queue.Contains(assetPath) && _invalidation.Contains(assetPath)) 340 | { 341 | _invalidation.RemoveAll(inList => inList == assetPath); 342 | _queue.Enqueue(assetPath); 343 | } 344 | return _pathToTexture[assetPath]; 345 | } 346 | 347 | var texture = new Texture2D(300, 300, TextureFormat.RGB24, true); 348 | _pathToTexture[assetPath] = texture; // TODO: Dimensions 349 | 350 | _queue.Enqueue(assetPath); 351 | 352 | return texture; 353 | } 354 | 355 | public bool TryRender(GameObject root) 356 | { 357 | if (_queue.Count == 0) return false; 358 | 359 | var originalAvatarGo = root; 360 | GameObject copy = null; 361 | var wasActive = originalAvatarGo.activeSelf; 362 | try 363 | { 364 | copy = Object.Instantiate(originalAvatarGo); 365 | copy.SetActive(true); 366 | originalAvatarGo.SetActive(false); 367 | Render(copy); 368 | } 369 | finally 370 | { 371 | if (wasActive) originalAvatarGo.SetActive(true); 372 | if (copy != null) Object.DestroyImmediate(copy); 373 | } 374 | 375 | return true; 376 | } 377 | 378 | private void Render(GameObject copy) 379 | { 380 | var viewer = new AnimationViewerGenerator(); 381 | try 382 | { 383 | viewer.Begin(copy); 384 | var animator = copy.GetComponent(); 385 | if (animator.isHuman && _bone != HumanBodyBones.LastBone) 386 | { 387 | var head = animator.GetBoneTransform(_bone); 388 | viewer.ParentCameraTo(head); 389 | } 390 | else 391 | { 392 | viewer.ParentCameraTo(animator.transform); 393 | } 394 | 395 | var itemCount = 0; 396 | while (_queue.Count > 0 && itemCount < _queueSize) 397 | { 398 | var path = _queue.Dequeue(); 399 | var clip = AssetDatabase.LoadAssetAtPath(path); 400 | if (clip == null) clip = new AnimationClip(); // Defensive: Might happen if the clip gets deleted during an update 401 | if (_basePose != null) 402 | { 403 | var modifiedClip = Object.Instantiate(clip); 404 | var missingBindings = AnimationUtility.GetCurveBindings(_basePose) 405 | .Where(binding => AnimationUtility.GetEditorCurve(clip, binding) == null) 406 | .ToArray(); 407 | foreach (var missingBinding in missingBindings) 408 | { 409 | AnimationUtility.SetEditorCurve(modifiedClip, missingBinding, AnimationUtility.GetEditorCurve(_basePose, missingBinding)); 410 | } 411 | viewer.Render(modifiedClip, _pathToTexture[path], _normalizedTime); 412 | } 413 | else 414 | { 415 | viewer.Render(clip, _pathToTexture[path], _normalizedTime); 416 | } 417 | 418 | // This is a workaround for an issue where the muscles will not update 419 | // across multiple samplings of the animator on the same frame. 420 | // This issue is mainly visible when the update speed (number of animation 421 | // clips updated per frame) is greater than 1. 422 | // By disabling and enabling the animator copy, this allows us to resample it. 423 | copy.SetActive(false); 424 | copy.SetActive(true); 425 | 426 | itemCount++; 427 | } 428 | } 429 | finally 430 | { 431 | viewer.Terminate(); 432 | } 433 | } 434 | 435 | public void QueueSize(int queueSize) 436 | { 437 | _queueSize = queueSize; 438 | } 439 | 440 | public void Bone(HumanBodyBones bone) 441 | { 442 | _bone = bone; 443 | } 444 | 445 | public void NormalizedTime(float normalizedTime) 446 | { 447 | _normalizedTime = normalizedTime; 448 | } 449 | 450 | public void BasePose(AnimationClip basePose) 451 | { 452 | _basePose = basePose; 453 | } 454 | } 455 | } -------------------------------------------------------------------------------- /Assets/Hai/AnimationViewer/Scripts/Editor/AnimationViewerEditorWindow.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: ba85972e78754e2096cfa520a64c42a8 3 | timeCreated: 1643726214 -------------------------------------------------------------------------------- /Assets/Hai/AnimationViewer/Scripts/Editor/AnimationViewerGenerator.cs: -------------------------------------------------------------------------------- 1 | using UnityEditor; 2 | using UnityEngine; 3 | 4 | namespace Hai.AnimationViewer.Scripts.Editor 5 | { 6 | public class AnimationViewerGenerator 7 | { 8 | private GameObject _animatedRoot; 9 | private Camera _camera; 10 | 11 | public void Begin(GameObject animatedRoot) 12 | { 13 | _animatedRoot = animatedRoot; 14 | 15 | _camera = new GameObject().AddComponent(); 16 | 17 | var sceneCamera = SceneView.lastActiveSceneView.camera; 18 | _camera.transform.position = sceneCamera.transform.position; 19 | _camera.transform.rotation = sceneCamera.transform.rotation; 20 | var whRatio = (1f * sceneCamera.pixelWidth / sceneCamera.pixelHeight); 21 | _camera.fieldOfView = whRatio < 1 ? sceneCamera.fieldOfView * whRatio : sceneCamera.fieldOfView; 22 | _camera.orthographic = sceneCamera.orthographic; 23 | _camera.nearClipPlane = sceneCamera.nearClipPlane; 24 | _camera.farClipPlane = sceneCamera.farClipPlane; 25 | _camera.orthographicSize = sceneCamera.orthographicSize; 26 | } 27 | 28 | public void ParentCameraTo(Transform newParent) 29 | { 30 | _camera.transform.parent = newParent; 31 | } 32 | 33 | public void Terminate() 34 | { 35 | Object.DestroyImmediate(_camera.gameObject); 36 | } 37 | 38 | public void Render(AnimationClip clip, Texture2D element, float normalizedTime) 39 | { 40 | var initPos = _animatedRoot.transform.position; 41 | var initRot = _animatedRoot.transform.rotation; 42 | try 43 | { 44 | AnimationMode.StartAnimationMode(); 45 | AnimationMode.BeginSampling(); 46 | AnimationMode.SampleAnimationClip(_animatedRoot.gameObject, clip, normalizedTime * clip.length); 47 | AnimationMode.EndSampling(); 48 | // This is a workaround for an issue where for some reason, the animator moves to the origin 49 | // after sampling despite the animation having no RootT/RootQ properties. 50 | _animatedRoot.transform.position = initPos; 51 | _animatedRoot.transform.rotation = initRot; 52 | 53 | var renderTexture = RenderTexture.GetTemporary(element.width, element.height, 24); 54 | renderTexture.wrapMode = TextureWrapMode.Clamp; 55 | 56 | RenderCamera(renderTexture, _camera); 57 | RenderTextureTo(renderTexture, element); 58 | RenderTexture.ReleaseTemporary(renderTexture); 59 | } 60 | finally 61 | { 62 | AnimationMode.StopAnimationMode(); 63 | _animatedRoot.transform.position = initPos; 64 | _animatedRoot.transform.rotation = initRot; 65 | } 66 | } 67 | 68 | private static void RenderCamera(RenderTexture renderTexture, Camera camera) 69 | { 70 | var originalRenderTexture = camera.targetTexture; 71 | var originalAspect = camera.aspect; 72 | try 73 | { 74 | camera.targetTexture = renderTexture; 75 | camera.aspect = (float) renderTexture.width / renderTexture.height; 76 | camera.Render(); 77 | } 78 | finally 79 | { 80 | camera.targetTexture = originalRenderTexture; 81 | camera.aspect = originalAspect; 82 | } 83 | } 84 | 85 | private static void RenderTextureTo(RenderTexture renderTexture, Texture2D texture2D) 86 | { 87 | RenderTexture.active = renderTexture; 88 | texture2D.ReadPixels(new Rect(0, 0, renderTexture.width, renderTexture.height), 0, 0); 89 | texture2D.Apply(); 90 | RenderTexture.active = null; 91 | } 92 | } 93 | } -------------------------------------------------------------------------------- /Assets/Hai/AnimationViewer/Scripts/Editor/AnimationViewerGenerator.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 26cc97c32dab4925a0180c51b258950d 3 | timeCreated: 1643714908 -------------------------------------------------------------------------------- /Assets/Hai/BlendshapeViewer.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c250276ee4eb4ca1a97370171310afe0 3 | timeCreated: 1642187788 -------------------------------------------------------------------------------- /Assets/Hai/BlendshapeViewer/Scripts.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: d02e3cf86dbd4dbd8c91b37bd9ff0d36 3 | timeCreated: 1642188055 -------------------------------------------------------------------------------- /Assets/Hai/BlendshapeViewer/Scripts/Editor.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 3cc19adda8b9439e8ee093deb6b7672c 3 | timeCreated: 1642187828 -------------------------------------------------------------------------------- /Assets/Hai/BlendshapeViewer/Scripts/Editor/BlendshapeViewerDiffCompute.cs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 Pema Malling 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | */ 10 | 11 | using System.Linq; 12 | using UnityEditor; 13 | using UnityEngine; 14 | 15 | namespace Hai.BlendshapeViewer.Scripts.Editor 16 | { 17 | public class BlendshapeViewerDiffCompute 18 | { 19 | private readonly ComputeShader _computeShader; 20 | private readonly ComputeBuffer _buf; 21 | private readonly int _kernel; 22 | 23 | public BlendshapeViewerDiffCompute() 24 | { 25 | _computeShader = FindComputeShader(); 26 | _kernel = _computeShader.FindKernel("DiffCompute"); 27 | _buf = new ComputeBuffer(4, sizeof(int)); 28 | _computeShader.SetBuffer(_kernel, "ResultBuffer", _buf); 29 | } 30 | 31 | public Vector4 Compute(Texture2D textureA, Texture2D textureB) 32 | { 33 | var results = new int[4]; 34 | _buf.SetData(results); 35 | var computeShader = _computeShader; 36 | 37 | computeShader.SetTexture(_kernel, "InputA", textureA); 38 | computeShader.SetTexture(_kernel, "InputB", textureB); 39 | 40 | computeShader.Dispatch(_kernel, textureA.width / 8, textureB.height / 8, 1); 41 | 42 | _buf.GetData(results); 43 | return new Vector4(results[0], results[1], results[2], results[3]); 44 | } 45 | 46 | public void Terminate() 47 | { 48 | _buf.Release(); 49 | } 50 | 51 | private static ComputeShader FindComputeShader() 52 | { 53 | var assetPathOrEmpty = AssetDatabase.GUIDToAssetPath("569e5a4e6b0efc74b93a42db6d069724"); 54 | var defaultPath = "Assets/Hai/BlendshapeViewer/Scripts/Editor/DiffCompute.compute"; 55 | var computeShader = AssetDatabase.LoadAssetAtPath(assetPathOrEmpty == "" ? defaultPath : assetPathOrEmpty) 56 | ?? FindAmongAllComputeShaders(); 57 | return computeShader; 58 | } 59 | 60 | private static ComputeShader FindAmongAllComputeShaders() 61 | { 62 | return Resources.FindObjectsOfTypeAll() 63 | .First(o => o.name.Contains("DiffCompute")); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Assets/Hai/BlendshapeViewer/Scripts/Editor/BlendshapeViewerDiffCompute.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f8766d6d210046fb8561664f3c9bdf5b 3 | timeCreated: 1642204929 -------------------------------------------------------------------------------- /Assets/Hai/BlendshapeViewer/Scripts/Editor/BlendshapeViewerEditorWindow.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using UnityEditor; 4 | using UnityEngine; 5 | 6 | namespace Hai.BlendshapeViewer.Scripts.Editor 7 | { 8 | public class BlendshapeViewerEditorWindow : EditorWindow 9 | { 10 | private const int MinWidth = 150; 11 | public SkinnedMeshRenderer skinnedMesh; 12 | public bool showDifferences = true; 13 | public bool autoUpdateOnFocus = true; 14 | public int thumbnailSize = 100; 15 | public bool showHotspots; 16 | public bool useComputeShader = true; 17 | public Texture2D[] tex2ds = new Texture2D[0]; 18 | private Vector2 _scrollPos; 19 | private SkinnedMeshRenderer _generatedFor; 20 | private int _generatedSize; 21 | 22 | private Vector3 _generatedTransformPosition; 23 | private Quaternion _generatedTransformRotation; 24 | private float _generatedFieldOfView; 25 | private bool _generatedOrthographic; 26 | private float _generatedNearClipPlane; 27 | private float _generatedFarClipPlane; 28 | private float _generatedOrthographicSize; 29 | private Rect m_area; 30 | 31 | public BlendshapeViewerEditorWindow() 32 | { 33 | titleContent = new GUIContent("BlendshapeViewer"); 34 | } 35 | 36 | private void OnFocus() 37 | { 38 | if (!autoUpdateOnFocus) return; 39 | if (skinnedMesh == null) return; 40 | if (!HasGenerationParamsChanged()) return; 41 | 42 | TryExecuteUpdate(); 43 | } 44 | 45 | private void OnGUI() 46 | { 47 | var serializedObject = new SerializedObject(this); 48 | EditorGUILayout.PropertyField(serializedObject.FindProperty(nameof(skinnedMesh))); 49 | EditorGUILayout.BeginHorizontal(); 50 | EditorGUILayout.PropertyField(serializedObject.FindProperty(nameof(showDifferences))); 51 | if (showDifferences) 52 | { 53 | EditorGUILayout.PropertyField(serializedObject.FindProperty(nameof(showHotspots))); 54 | } 55 | EditorGUILayout.EndHorizontal(); 56 | EditorGUILayout.BeginHorizontal(); 57 | EditorGUILayout.PropertyField(serializedObject.FindProperty(nameof(autoUpdateOnFocus))); 58 | if (SystemInfo.supportsComputeShaders) 59 | { 60 | EditorGUILayout.PropertyField(serializedObject.FindProperty(nameof(useComputeShader))); 61 | } 62 | EditorGUILayout.EndHorizontal(); 63 | EditorGUILayout.IntSlider(serializedObject.FindProperty(nameof(thumbnailSize)), 100, 300); 64 | 65 | EditorGUI.BeginDisabledGroup(skinnedMesh == null || AnimationMode.InAnimationMode()); 66 | if (GUILayout.Button("Update")) 67 | { 68 | TryExecuteUpdate(); 69 | } 70 | EditorGUI.EndDisabledGroup(); 71 | serializedObject.ApplyModifiedProperties(); 72 | 73 | var width = Mathf.Max(_generatedSize, MinWidth); 74 | var total = tex2ds.Length; 75 | if (skinnedMesh != null && total > 0 && _generatedFor == skinnedMesh) 76 | { 77 | var serializedSkinnedMesh = new SerializedObject(skinnedMesh); 78 | 79 | _scrollPos = GUILayout.BeginScrollView(_scrollPos, GUILayout.Height(position.height - EditorGUIUtility.singleLineHeight * 7)); 80 | 81 | var mod = Mathf.Max(1, (int)position.width / (width + 15)); 82 | var highlightColor = EditorGUIUtility.isProSkin ? new Color(0.92f, 0.62f, 0.25f) : new Color(0.74f, 0.47f, 0.1f); 83 | for (var index = 0; index < total; index++) 84 | { 85 | var texture2D = tex2ds[index]; 86 | if (index % mod == 0) 87 | { 88 | EditorGUILayout.BeginHorizontal(); 89 | GUILayout.FlexibleSpace(); 90 | } 91 | 92 | EditorGUILayout.BeginVertical(); 93 | GUILayout.Box(texture2D); 94 | var blendShapeName = skinnedMesh.sharedMesh.GetBlendShapeName(index); 95 | var weight = serializedSkinnedMesh.FindProperty("m_BlendShapeWeights").GetArrayElementAtIndex(index); 96 | var isNonZero = weight.floatValue > 0f; 97 | Colored(isNonZero, highlightColor, () => 98 | { 99 | EditorGUILayout.TextField(blendShapeName, isNonZero ? EditorStyles.boldLabel : EditorStyles.label, GUILayout.Width(width)); 100 | }); 101 | EditorGUILayout.Slider(weight, 0f, 100f, GUIContent.none, GUILayout.Width(width)); 102 | EditorGUILayout.EndVertical(); 103 | 104 | if ((index + 1) % mod == 0 || index == total - 1) 105 | { 106 | GUILayout.FlexibleSpace(); 107 | EditorGUILayout.EndHorizontal(); 108 | } 109 | } 110 | 111 | GUILayout.EndScrollView(); 112 | serializedSkinnedMesh.ApplyModifiedProperties(); 113 | } 114 | } 115 | 116 | private void TryExecuteUpdate() 117 | { 118 | if (AnimationMode.InAnimationMode()) return; 119 | 120 | Generate(); 121 | SaveGenerationParams(); 122 | } 123 | 124 | private void SaveGenerationParams() 125 | { 126 | _generatedFor = skinnedMesh; 127 | _generatedSize = thumbnailSize; 128 | 129 | var sceneCamera = SceneView.lastActiveSceneView.camera; 130 | _generatedTransformPosition = sceneCamera.transform.position; 131 | _generatedTransformRotation = sceneCamera.transform.rotation; 132 | var whRatio = (1f * sceneCamera.pixelWidth / sceneCamera.pixelHeight); 133 | _generatedFieldOfView = whRatio < 1 ? sceneCamera.fieldOfView * whRatio : sceneCamera.fieldOfView; 134 | _generatedOrthographic = sceneCamera.orthographic; 135 | _generatedNearClipPlane = sceneCamera.nearClipPlane; 136 | _generatedFarClipPlane = sceneCamera.farClipPlane; 137 | _generatedOrthographicSize = sceneCamera.orthographicSize; 138 | } 139 | 140 | private bool HasGenerationParamsChanged() 141 | { 142 | var sceneCamera = SceneView.lastActiveSceneView.camera; 143 | if (_generatedTransformPosition != sceneCamera.transform.position) return true; 144 | if (_generatedTransformRotation != sceneCamera.transform.rotation) return true; 145 | var whRatio = (1f * sceneCamera.pixelWidth / sceneCamera.pixelHeight); 146 | if (Math.Abs(_generatedFieldOfView - (whRatio < 1 ? sceneCamera.fieldOfView * whRatio : sceneCamera.fieldOfView)) > 0.001f) return true; 147 | if (_generatedOrthographic != sceneCamera.orthographic) return true; 148 | if (Math.Abs(_generatedNearClipPlane - sceneCamera.nearClipPlane) > 0.001f) return true; 149 | if (Math.Abs(_generatedFarClipPlane - sceneCamera.farClipPlane) > 0.001f) return true; 150 | if (Math.Abs(_generatedOrthographicSize - sceneCamera.orthographicSize) > 0.001f) return true; 151 | return false; 152 | } 153 | 154 | private void UsingSkinnedMesh(SkinnedMeshRenderer inSkinnedMesh) 155 | { 156 | skinnedMesh = inSkinnedMesh; 157 | } 158 | 159 | private void Generate() 160 | { 161 | var module = new BlendshapeViewerGenerator(); 162 | try 163 | { 164 | module.Begin(skinnedMesh, showHotspots ? 0.95f : 0, useComputeShader); 165 | Texture2D neutralTexture = null; 166 | if (showDifferences) 167 | { 168 | neutralTexture = NewTexture(); 169 | module.Render(EmptyClip(), neutralTexture); 170 | } 171 | 172 | var results = new [] {skinnedMesh} 173 | .SelectMany(relevantSmr => 174 | { 175 | var sharedMesh = relevantSmr.sharedMesh; 176 | 177 | return Enumerable.Range(0, sharedMesh.blendShapeCount) 178 | .Select(i => 179 | { 180 | var blendShapeName = sharedMesh.GetBlendShapeName(i); 181 | var currentWeight = relevantSmr.GetBlendShapeWeight(i); 182 | 183 | // If the user has already animated this to 100, in normal circumstances the diff would show nothing. 184 | // Animate the blendshape to 0 instead so that a diff can be generated. 185 | var isAlreadyAnimatedTo100 = Math.Abs(currentWeight - 100f) < 0.001f; 186 | var tempClip = new AnimationClip(); 187 | AnimationUtility.SetEditorCurve( 188 | tempClip, 189 | new EditorCurveBinding 190 | { 191 | path = "", 192 | type = typeof(SkinnedMeshRenderer), 193 | propertyName = $"blendShape.{blendShapeName}" 194 | }, 195 | AnimationCurve.Constant(0, 1 / 60f, isAlreadyAnimatedTo100 ? 0f : 100f) 196 | ); 197 | 198 | return tempClip; 199 | }) 200 | .ToArray(); 201 | }) 202 | .ToArray(); 203 | 204 | tex2ds = results 205 | .Select((clip, i) => 206 | { 207 | if (i % 10 == 0) EditorUtility.DisplayProgressBar("Rendering", $"Rendering ({i} / {results.Length})", 1f * i / results.Length); 208 | 209 | var currentWeight = skinnedMesh.GetBlendShapeWeight(i); 210 | var isAlreadyAnimatedTo100 = Math.Abs(currentWeight - 100f) < 0.001f; 211 | 212 | var result = NewTexture(); 213 | module.Render(clip, result); 214 | if (i == 0) 215 | { 216 | // Workaround a weird bug where the first blendshape is always incorrectly rendered 217 | module.Render(clip, result); 218 | } 219 | if (showDifferences) 220 | { 221 | if (isAlreadyAnimatedTo100) 222 | { 223 | module.Diff(neutralTexture, result, result); 224 | } 225 | else 226 | { 227 | module.Diff(result, neutralTexture, result); 228 | } 229 | } 230 | return result; 231 | }) 232 | .ToArray(); 233 | } 234 | finally 235 | { 236 | module.Terminate(); 237 | EditorUtility.ClearProgressBar(); 238 | } 239 | } 240 | 241 | private static void Colored(bool isActive, Color bgColor, Action inside) 242 | { 243 | var col = GUI.contentColor; 244 | try 245 | { 246 | if (isActive) GUI.contentColor = bgColor; 247 | inside(); 248 | } 249 | finally 250 | { 251 | GUI.contentColor = col; 252 | } 253 | } 254 | 255 | private static AnimationClip EmptyClip() 256 | { 257 | var emptyClip = new AnimationClip(); 258 | AnimationUtility.SetEditorCurve( 259 | emptyClip, 260 | new EditorCurveBinding 261 | { 262 | path = "_ignored", 263 | type = typeof(GameObject), 264 | propertyName = "m_Active" 265 | }, 266 | AnimationCurve.Constant(0, 1 / 60f, 100f) 267 | ); 268 | return emptyClip; 269 | } 270 | 271 | private Texture2D NewTexture() 272 | { 273 | var newTexture = new Texture2D(Mathf.Max(thumbnailSize, MinWidth), thumbnailSize, TextureFormat.RGB24, false); 274 | newTexture.wrapMode = TextureWrapMode.Clamp; 275 | return newTexture; 276 | } 277 | 278 | [MenuItem("Window/Haï/BlendshapeViewer")] 279 | public static void ShowWindow() 280 | { 281 | Obtain().Show(); 282 | } 283 | 284 | [MenuItem("CONTEXT/SkinnedMeshRenderer/Haï BlendshapeViewer")] 285 | public static void OpenEditor(MenuCommand command) 286 | { 287 | var window = Obtain(); 288 | window.UsingSkinnedMesh((SkinnedMeshRenderer) command.context); 289 | window.Show(); 290 | window.TryExecuteUpdate(); 291 | } 292 | 293 | private static BlendshapeViewerEditorWindow Obtain() 294 | { 295 | var editor = GetWindow(false, null, false); 296 | editor.titleContent = new GUIContent("BlendshapeViewer"); 297 | return editor; 298 | } 299 | } 300 | } -------------------------------------------------------------------------------- /Assets/Hai/BlendshapeViewer/Scripts/Editor/BlendshapeViewerEditorWindow.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f8a00e306fa94a84914e80755b10934e 3 | timeCreated: 1642187828 -------------------------------------------------------------------------------- /Assets/Hai/BlendshapeViewer/Scripts/Editor/BlendshapeViewerGenerator.cs: -------------------------------------------------------------------------------- 1 | using UnityEditor; 2 | using UnityEngine; 3 | 4 | namespace Hai.BlendshapeViewer.Scripts.Editor 5 | { 6 | public class BlendshapeViewerGenerator 7 | { 8 | private Material _material; 9 | private SkinnedMeshRenderer _skinnedMesh; 10 | private bool _useComputeShader; 11 | private Camera _camera; 12 | private float _overlay; 13 | private BlendshapeViewerDiffCompute _diffCompute; 14 | 15 | public void Begin(SkinnedMeshRenderer skinnedMesh, float overlay, bool useComputeShader) 16 | { 17 | _skinnedMesh = skinnedMesh; 18 | _overlay = overlay; 19 | _useComputeShader = SystemInfo.supportsComputeShaders && useComputeShader; 20 | 21 | _material = new Material(_useComputeShader ? Shader.Find("Hai/BlendshapeViewerRectOnly") : Shader.Find("Hai/BlendshapeViewer")); 22 | _material.SetFloat("_Hotspots", _overlay); 23 | _camera = new GameObject().AddComponent(); 24 | 25 | var sceneCamera = SceneView.lastActiveSceneView.camera; 26 | _camera.transform.position = sceneCamera.transform.position; 27 | _camera.transform.rotation = sceneCamera.transform.rotation; 28 | var whRatio = (1f * sceneCamera.pixelWidth / sceneCamera.pixelHeight); 29 | _camera.fieldOfView = whRatio < 1 ? sceneCamera.fieldOfView * whRatio : sceneCamera.fieldOfView; 30 | _camera.orthographic = sceneCamera.orthographic; 31 | _camera.nearClipPlane = sceneCamera.nearClipPlane; 32 | _camera.farClipPlane = sceneCamera.farClipPlane; 33 | _camera.orthographicSize = sceneCamera.orthographicSize; 34 | 35 | if (_useComputeShader) 36 | { 37 | _diffCompute = new BlendshapeViewerDiffCompute(); 38 | } 39 | } 40 | 41 | public void Terminate() 42 | { 43 | Object.DestroyImmediate(_material); 44 | Object.DestroyImmediate(_camera.gameObject); 45 | if (_useComputeShader) 46 | { 47 | _diffCompute.Terminate(); 48 | } 49 | } 50 | 51 | public void Render(AnimationClip clip, Texture2D element) 52 | { 53 | try 54 | { 55 | AnimationMode.StartAnimationMode(); 56 | AnimationMode.BeginSampling(); 57 | AnimationMode.SampleAnimationClip(_skinnedMesh.gameObject, clip, 1 / 60f); 58 | AnimationMode.EndSampling(); 59 | 60 | var renderTexture = RenderTexture.GetTemporary(element.width, element.height, 24); 61 | renderTexture.wrapMode = TextureWrapMode.Clamp; 62 | 63 | RenderCamera(renderTexture, _camera); 64 | RenderTextureTo(renderTexture, element); 65 | RenderTexture.ReleaseTemporary(renderTexture); 66 | } 67 | finally 68 | { 69 | AnimationMode.StopAnimationMode(); 70 | } 71 | } 72 | 73 | private static void RenderCamera(RenderTexture renderTexture, Camera camera) 74 | { 75 | var originalRenderTexture = camera.targetTexture; 76 | var originalAspect = camera.aspect; 77 | try 78 | { 79 | camera.targetTexture = renderTexture; 80 | camera.aspect = (float) renderTexture.width / renderTexture.height; 81 | camera.Render(); 82 | } 83 | finally 84 | { 85 | camera.targetTexture = originalRenderTexture; 86 | camera.aspect = originalAspect; 87 | } 88 | } 89 | 90 | private static void RenderTextureTo(RenderTexture renderTexture, Texture2D texture2D) 91 | { 92 | RenderTexture.active = renderTexture; 93 | texture2D.ReadPixels(new Rect(0, 0, renderTexture.width, renderTexture.height), 0, 0); 94 | texture2D.Apply(); 95 | RenderTexture.active = null; 96 | } 97 | 98 | public void Diff(Texture2D source, Texture2D neutralTexture, Texture2D newTexture) 99 | { 100 | var diff = RenderTexture.GetTemporary(newTexture.width, newTexture.height, 24); 101 | _material.SetTexture("_NeutralTex", neutralTexture); 102 | if (_useComputeShader) 103 | { 104 | _material.SetVector("_Rect", _diffCompute.Compute(source, neutralTexture)); 105 | } 106 | Graphics.Blit(source, diff, _material); 107 | RenderTextureTo(diff, newTexture); 108 | RenderTexture.ReleaseTemporary(diff); 109 | } 110 | } 111 | } -------------------------------------------------------------------------------- /Assets/Hai/BlendshapeViewer/Scripts/Editor/BlendshapeViewerGenerator.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 8d4cd89b41ec45ec99990d128653fdee 3 | timeCreated: 1642188144 -------------------------------------------------------------------------------- /Assets/Hai/BlendshapeViewer/Scripts/Editor/DiffCompute.compute: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 Pema Malling 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | */ 10 | 11 | #pragma kernel DiffCompute 12 | 13 | Texture2D InputA; 14 | Texture2D InputB; 15 | RWStructuredBuffer ResultBuffer; // [minX, minY, maxX, maxY] 16 | 17 | [numthreads(8,8,1)] 18 | void DiffCompute (uint3 id : SV_DispatchThreadID) 19 | { 20 | float3 diff = InputA[id.xy].xyz - InputB[id.xy].xyz; 21 | if (dot(diff, diff) > 0.01) 22 | { 23 | // handle first occurrence, this branch needs to be here because stupid compiler 24 | if (ResultBuffer[0] == 0) 25 | { 26 | InterlockedCompareStore(ResultBuffer[0], 0, id.x); 27 | InterlockedCompareStore(ResultBuffer[1], 0, id.y); 28 | InterlockedCompareStore(ResultBuffer[2], 0, id.x); 29 | InterlockedCompareStore(ResultBuffer[3], 0, id.y); 30 | } 31 | 32 | // calc AABB points 33 | { 34 | InterlockedMin(ResultBuffer[0], id.x); 35 | InterlockedMin(ResultBuffer[1], id.y); 36 | InterlockedMax(ResultBuffer[2], id.x); 37 | InterlockedMax(ResultBuffer[3], id.y); 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /Assets/Hai/BlendshapeViewer/Scripts/Editor/DiffCompute.compute.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 569e5a4e6b0efc74b93a42db6d069724 3 | ComputeShaderImporter: 4 | externalObjects: {} 5 | currentAPIMask: 4 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Assets/Hai/BlendshapeViewer/Shaders.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 2f7154c0baf143e0a73238d5eae7dc34 3 | timeCreated: 1642187788 -------------------------------------------------------------------------------- /Assets/Hai/BlendshapeViewer/Shaders/HaiBlendshapeViewer.shader: -------------------------------------------------------------------------------- 1 | Shader "Hai/BlendshapeViewer" 2 | { 3 | Properties 4 | { 5 | _MainTex ("Morphed Texture", 2D) = "white" {} 6 | _NeutralTex ("Neutral Texture", 2D) = "white" {} 7 | _Hotspots ("Hotspots", Range(0, 1)) = 0 8 | } 9 | SubShader 10 | { 11 | Tags { "RenderType"="Opaque" } 12 | LOD 100 13 | 14 | Pass 15 | { 16 | CGPROGRAM 17 | #pragma vertex vert 18 | #pragma fragment frag 19 | 20 | #include "UnityCG.cginc" 21 | 22 | struct appdata 23 | { 24 | float4 vertex : POSITION; 25 | float2 uv : TEXCOORD0; 26 | }; 27 | 28 | struct v2f 29 | { 30 | float2 uv : TEXCOORD0; 31 | UNITY_FOG_COORDS(1) 32 | float4 vertex : SV_POSITION; 33 | float4 diff : TEXCOORD1; 34 | }; 35 | 36 | sampler2D _MainTex; 37 | float4 _MainTex_ST; 38 | float4 _MainTex_TexelSize; 39 | 40 | sampler2D _NeutralTex; 41 | float4 _NeutralTex_ST; 42 | 43 | float _Hotspots; 44 | 45 | v2f vert (appdata v) 46 | { 47 | v2f o; 48 | o.vertex = UnityObjectToClipPos(v.vertex); 49 | o.uv = TRANSFORM_TEX(v.uv, _MainTex); 50 | 51 | float width = _MainTex_TexelSize.z; 52 | float height = _MainTex_TexelSize.w; 53 | float4 difference = float4(width, height, 0, 0); 54 | // TODO: There's gotta be a more clever way to do this that uses parallelism 55 | for (int y = 0; y < height; y++) { 56 | for (int x = 0; x < width; x++) { 57 | float4 sampleLocation = float4(x / width, y / height, 0, 0); 58 | fixed3 neutral = tex2Dlod(_NeutralTex, sampleLocation).xyz; 59 | fixed3 morphed = tex2Dlod(_MainTex, sampleLocation).xyz; 60 | fixed3 v = neutral - morphed; 61 | if (dot(v, v) > 0.01) { 62 | difference = float4( 63 | min(x, difference.x), 64 | min(y, difference.y), 65 | max(x, difference.z), 66 | max(y, difference.w) 67 | ); 68 | } 69 | } 70 | } 71 | o.diff = difference; 72 | 73 | return o; 74 | } 75 | 76 | fixed4 frag (v2f i) : SV_Target 77 | { 78 | float width = _MainTex_TexelSize.z; 79 | float height = _MainTex_TexelSize.w; 80 | 81 | float4 difference = i.diff; 82 | difference = difference + float4(-1, -1, 1, 1) * 2; // Margin 83 | 84 | fixed4 col = tex2D(_MainTex, i.uv); 85 | if (i.uv.x < difference.x / width || i.uv.x > difference.z / width 86 | || i.uv.y < difference.y / height || i.uv.y > difference.w / height) { 87 | col.xyz = col.xyz * 0.2; 88 | } 89 | if (_Hotspots > 0.01) { 90 | fixed3 neutral = tex2D(_NeutralTex, i.uv).xyz; 91 | fixed3 morphed = tex2D(_MainTex, i.uv).xyz; 92 | col = lerp(col, length(neutral - morphed) * float4(1, 0, 0, 1), _Hotspots); 93 | } 94 | return col; 95 | } 96 | ENDCG 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Assets/Hai/BlendshapeViewer/Shaders/HaiBlendshapeViewer.shader.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 1771896a2c7542debf65f1e264b535d7 3 | timeCreated: 1642187788 -------------------------------------------------------------------------------- /Assets/Hai/BlendshapeViewer/Shaders/HaiBlendshapeViewerRectOnly.shader: -------------------------------------------------------------------------------- 1 | Shader "Hai/BlendshapeViewerRectOnly" 2 | { 3 | Properties 4 | { 5 | _MainTex ("Morphed Texture", 2D) = "white" {} 6 | _NeutralTex ("Neutral Texture", 2D) = "white" {} 7 | _Hotspots ("Hotspots", Range(0, 1)) = 0 8 | _Rect ("Rect", Vector) = (0, 0, 0, 0) 9 | } 10 | SubShader 11 | { 12 | Tags { "RenderType"="Opaque" } 13 | LOD 100 14 | 15 | Pass 16 | { 17 | CGPROGRAM 18 | #pragma vertex vert 19 | #pragma fragment frag 20 | 21 | #include "UnityCG.cginc" 22 | 23 | struct appdata 24 | { 25 | float4 vertex : POSITION; 26 | float2 uv : TEXCOORD0; 27 | }; 28 | 29 | struct v2f 30 | { 31 | float2 uv : TEXCOORD0; 32 | UNITY_FOG_COORDS(1) 33 | float4 vertex : SV_POSITION; 34 | }; 35 | 36 | sampler2D _MainTex; 37 | float4 _MainTex_ST; 38 | float4 _MainTex_TexelSize; 39 | 40 | sampler2D _NeutralTex; 41 | float4 _NeutralTex_ST; 42 | 43 | float _Hotspots; 44 | float4 _Rect; 45 | 46 | v2f vert (appdata v) 47 | { 48 | v2f o; 49 | o.vertex = UnityObjectToClipPos(v.vertex); 50 | o.uv = TRANSFORM_TEX(v.uv, _MainTex); 51 | 52 | return o; 53 | } 54 | 55 | fixed4 frag (v2f i) : SV_Target 56 | { 57 | float width = _MainTex_TexelSize.z; 58 | float height = _MainTex_TexelSize.w; 59 | 60 | float4 difference = _Rect; 61 | difference = difference + float4(-1, -1, 1, 1) * 2; // Margin 62 | 63 | fixed4 col = tex2D(_MainTex, i.uv); 64 | if (i.uv.x < difference.x / width || i.uv.x > difference.z / width 65 | || i.uv.y < difference.y / height || i.uv.y > difference.w / height) { 66 | col.xyz = col.xyz * 0.2; 67 | } 68 | if (_Hotspots > 0.01) { 69 | fixed3 neutral = tex2D(_NeutralTex, i.uv).xyz; 70 | fixed3 morphed = tex2D(_MainTex, i.uv).xyz; 71 | col = lerp(col, length(neutral - morphed) * float4(1, 0, 0, 1), _Hotspots); 72 | } 73 | return col; 74 | } 75 | ENDCG 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Assets/Hai/BlendshapeViewer/Shaders/HaiBlendshapeViewerRectOnly.shader.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 45bbf6d4741e497690eb97657de88c57 3 | timeCreated: 1642206672 -------------------------------------------------------------------------------- /Assets/Hai/VisualExpressionsEditor.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: ef06eef694e8e9244a169d959865963a 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Assets/Hai/VisualExpressionsEditor/Scripts.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 8e1326e0542f6274797f64fccabe6e05 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Assets/Hai/VisualExpressionsEditor/Scripts/Editor.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 9632d531b1da8214ea4082c070a53b67 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Assets/Hai/VisualExpressionsEditor/Scripts/Editor/VEEDiffCompute.compute: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 Pema Malling 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | */ 10 | 11 | #pragma kernel VEEDiffCompute 12 | 13 | Texture2D InputA; 14 | Texture2D InputB; 15 | RWStructuredBuffer ResultBuffer; // [minX, minY, maxX, maxY] 16 | 17 | [numthreads(8,8,1)] 18 | void VEEDiffCompute (uint3 id : SV_DispatchThreadID) 19 | { 20 | float3 diff = InputA[id.xy].xyz - InputB[id.xy].xyz; 21 | if (dot(diff, diff) > 0.01) 22 | { 23 | // handle first occurrence, this branch needs to be here because stupid compiler 24 | if (ResultBuffer[0] == 0) 25 | { 26 | InterlockedCompareStore(ResultBuffer[0], 0, id.x); 27 | InterlockedCompareStore(ResultBuffer[1], 0, id.y); 28 | InterlockedCompareStore(ResultBuffer[2], 0, id.x); 29 | InterlockedCompareStore(ResultBuffer[3], 0, id.y); 30 | } 31 | 32 | // calc AABB points 33 | { 34 | InterlockedMin(ResultBuffer[0], id.x); 35 | InterlockedMin(ResultBuffer[1], id.y); 36 | InterlockedMax(ResultBuffer[2], id.x); 37 | InterlockedMax(ResultBuffer[3], id.y); 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /Assets/Hai/VisualExpressionsEditor/Scripts/Editor/VEEDiffCompute.compute.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 61638c5c8f9240a2a670ca3a790a5eb5 3 | timeCreated: 1646711618 -------------------------------------------------------------------------------- /Assets/Hai/VisualExpressionsEditor/Scripts/Editor/VisualExpressionsEditorDiffCompute.cs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 Pema Malling 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | */ 10 | 11 | using System.Linq; 12 | using UnityEditor; 13 | using UnityEngine; 14 | 15 | namespace Hai.VisualExpressionsEditor.Scripts.Editor 16 | { 17 | public class VisualExpressionsEditorDiffCompute 18 | { 19 | private readonly ComputeShader _computeShader; 20 | private readonly ComputeBuffer _buf; 21 | private readonly int _kernel; 22 | 23 | public VisualExpressionsEditorDiffCompute() 24 | { 25 | _computeShader = FindComputeShader(); 26 | _kernel = _computeShader.FindKernel("VEEDiffCompute"); 27 | _buf = new ComputeBuffer(4, sizeof(int)); 28 | _computeShader.SetBuffer(_kernel, "ResultBuffer", _buf); 29 | } 30 | 31 | public Vector4 Compute(Texture2D textureA, Texture2D textureB) 32 | { 33 | var results = new int[4]; 34 | _buf.SetData(results); 35 | var computeShader = _computeShader; 36 | 37 | computeShader.SetTexture(_kernel, "InputA", textureA); 38 | computeShader.SetTexture(_kernel, "InputB", textureB); 39 | 40 | computeShader.Dispatch(_kernel, textureA.width / 8, textureB.height / 8, 1); 41 | 42 | _buf.GetData(results); 43 | return new Vector4(results[0], results[1], results[2], results[3]); 44 | } 45 | 46 | public void Terminate() 47 | { 48 | _buf.Release(); 49 | } 50 | 51 | private static ComputeShader FindComputeShader() 52 | { 53 | var assetPathOrEmpty = AssetDatabase.GUIDToAssetPath("61638c5c8f9240a2a670ca3a790a5eb5"); 54 | var defaultPath = "Assets/Hai/VisualExpressionsEditor/Scripts/Editor/VEEDiffCompute.compute"; 55 | var computeShader = AssetDatabase.LoadAssetAtPath(assetPathOrEmpty == "" ? defaultPath : assetPathOrEmpty) 56 | ?? FindAmongAllComputeShaders(); 57 | return computeShader; 58 | } 59 | 60 | private static ComputeShader FindAmongAllComputeShaders() 61 | { 62 | return Resources.FindObjectsOfTypeAll() 63 | .First(o => o.name.Contains("VEEDiffCompute")); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Assets/Hai/VisualExpressionsEditor/Scripts/Editor/VisualExpressionsEditorDiffCompute.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: d38e1172e8eb43b28e6504b7b672846b 3 | timeCreated: 1646711521 -------------------------------------------------------------------------------- /Assets/Hai/VisualExpressionsEditor/Scripts/Editor/VisualExpressionsEditorGeneratorClip.cs: -------------------------------------------------------------------------------- 1 | using UnityEditor; 2 | using UnityEngine; 3 | 4 | namespace Hai.VisualExpressionsEditor.Scripts.Editor 5 | { 6 | public class VisualExpressionsEditorGeneratorClip 7 | { 8 | private GameObject _animatedRoot; 9 | private Camera _camera; 10 | 11 | public void Begin(GameObject animatedRoot) 12 | { 13 | _animatedRoot = animatedRoot; 14 | 15 | _camera = new GameObject().AddComponent(); 16 | 17 | var sceneCamera = SceneView.lastActiveSceneView.camera; 18 | _camera.transform.position = sceneCamera.transform.position; 19 | _camera.transform.rotation = sceneCamera.transform.rotation; 20 | var whRatio = (1f * sceneCamera.pixelWidth / sceneCamera.pixelHeight); 21 | _camera.fieldOfView = whRatio < 1 ? sceneCamera.fieldOfView * whRatio : sceneCamera.fieldOfView; 22 | _camera.orthographic = sceneCamera.orthographic; 23 | _camera.nearClipPlane = sceneCamera.nearClipPlane; 24 | _camera.farClipPlane = sceneCamera.farClipPlane; 25 | _camera.orthographicSize = sceneCamera.orthographicSize; 26 | } 27 | 28 | public void ParentCameraTo(Transform newParent) 29 | { 30 | _camera.transform.parent = newParent; 31 | } 32 | 33 | public void Terminate() 34 | { 35 | Object.DestroyImmediate(_camera.gameObject); 36 | } 37 | 38 | public void Render(AnimationClip clip, Texture2D element, float normalizedTime) 39 | { 40 | var initPos = _animatedRoot.transform.position; 41 | var initRot = _animatedRoot.transform.rotation; 42 | try 43 | { 44 | AnimationMode.StartAnimationMode(); 45 | AnimationMode.BeginSampling(); 46 | AnimationMode.SampleAnimationClip(_animatedRoot.gameObject, clip, normalizedTime * clip.length); 47 | AnimationMode.EndSampling(); 48 | // This is a workaround for an issue where for some reason, the animator moves to the origin 49 | // after sampling despite the animation having no RootT/RootQ properties. 50 | _animatedRoot.transform.position = initPos; 51 | _animatedRoot.transform.rotation = initRot; 52 | 53 | var renderTexture = RenderTexture.GetTemporary(element.width, element.height, 24); 54 | renderTexture.wrapMode = TextureWrapMode.Clamp; 55 | 56 | RenderCamera(renderTexture, _camera); 57 | RenderTextureTo(renderTexture, element); 58 | RenderTexture.ReleaseTemporary(renderTexture); 59 | } 60 | finally 61 | { 62 | AnimationMode.StopAnimationMode(); 63 | _animatedRoot.transform.position = initPos; 64 | _animatedRoot.transform.rotation = initRot; 65 | } 66 | } 67 | 68 | private static void RenderCamera(RenderTexture renderTexture, Camera camera) 69 | { 70 | var originalRenderTexture = camera.targetTexture; 71 | var originalAspect = camera.aspect; 72 | try 73 | { 74 | camera.targetTexture = renderTexture; 75 | camera.aspect = (float) renderTexture.width / renderTexture.height; 76 | camera.Render(); 77 | } 78 | finally 79 | { 80 | camera.targetTexture = originalRenderTexture; 81 | camera.aspect = originalAspect; 82 | } 83 | } 84 | 85 | private static void RenderTextureTo(RenderTexture renderTexture, Texture2D texture2D) 86 | { 87 | RenderTexture.active = renderTexture; 88 | texture2D.ReadPixels(new Rect(0, 0, renderTexture.width, renderTexture.height), 0, 0); 89 | texture2D.Apply(); 90 | RenderTexture.active = null; 91 | } 92 | } 93 | } -------------------------------------------------------------------------------- /Assets/Hai/VisualExpressionsEditor/Scripts/Editor/VisualExpressionsEditorGeneratorClip.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 709eef4baa934834b3da0317d8a87bac 3 | timeCreated: 1646713231 -------------------------------------------------------------------------------- /Assets/Hai/VisualExpressionsEditor/Scripts/Editor/VisualExpressionsEditorGeneratorSingular.cs: -------------------------------------------------------------------------------- 1 | using UnityEditor; 2 | using UnityEngine; 3 | 4 | namespace Hai.VisualExpressionsEditor.Scripts.Editor 5 | { 6 | public class VisualExpressionsEditorGeneratorSingular 7 | { 8 | private Material _material; 9 | private GameObject _objectToAnimate; 10 | private bool _useComputeShader; 11 | private Camera _camera; 12 | private float _overlay; 13 | private VisualExpressionsEditorDiffCompute _diffCompute; 14 | 15 | public void Begin(GameObject objectToAnimate, float overlay, bool useComputeShader) 16 | { 17 | _objectToAnimate = objectToAnimate; 18 | _overlay = overlay; 19 | _useComputeShader = SystemInfo.supportsComputeShaders && useComputeShader; 20 | 21 | _material = new Material(_useComputeShader ? Shader.Find("Hai/VisualExpressionsEditorRectOnly") : Shader.Find("Hai/VisualExpressionsEditor")); 22 | _material.SetFloat("_Hotspots", _overlay); 23 | _camera = new GameObject().AddComponent(); 24 | 25 | var sceneCamera = SceneView.lastActiveSceneView.camera; 26 | _camera.transform.position = sceneCamera.transform.position; 27 | _camera.transform.rotation = sceneCamera.transform.rotation; 28 | var whRatio = (1f * sceneCamera.pixelWidth / sceneCamera.pixelHeight); 29 | _camera.fieldOfView = whRatio < 1 ? sceneCamera.fieldOfView * whRatio : sceneCamera.fieldOfView; 30 | _camera.orthographic = sceneCamera.orthographic; 31 | _camera.nearClipPlane = sceneCamera.nearClipPlane; 32 | _camera.farClipPlane = sceneCamera.farClipPlane; 33 | _camera.orthographicSize = sceneCamera.orthographicSize; 34 | 35 | if (_useComputeShader) 36 | { 37 | _diffCompute = new VisualExpressionsEditorDiffCompute(); 38 | } 39 | } 40 | 41 | public void Terminate() 42 | { 43 | Object.DestroyImmediate(_material); 44 | Object.DestroyImmediate(_camera.gameObject); 45 | if (_useComputeShader) 46 | { 47 | _diffCompute.Terminate(); 48 | } 49 | } 50 | 51 | public void Render(AnimationClip clip, Texture2D element) 52 | { 53 | try 54 | { 55 | AnimationMode.StartAnimationMode(); 56 | AnimationMode.BeginSampling(); 57 | AnimationMode.SampleAnimationClip(_objectToAnimate.gameObject, clip, 1 / 60f); 58 | AnimationMode.EndSampling(); 59 | 60 | var renderTexture = RenderTexture.GetTemporary(element.width, element.height, 24); 61 | renderTexture.wrapMode = TextureWrapMode.Clamp; 62 | 63 | RenderCamera(renderTexture, _camera); 64 | RenderTextureTo(renderTexture, element); 65 | RenderTexture.ReleaseTemporary(renderTexture); 66 | } 67 | finally 68 | { 69 | AnimationMode.StopAnimationMode(); 70 | } 71 | } 72 | 73 | private static void RenderCamera(RenderTexture renderTexture, Camera camera) 74 | { 75 | var originalRenderTexture = camera.targetTexture; 76 | var originalAspect = camera.aspect; 77 | try 78 | { 79 | camera.targetTexture = renderTexture; 80 | camera.aspect = (float) renderTexture.width / renderTexture.height; 81 | camera.Render(); 82 | } 83 | finally 84 | { 85 | camera.targetTexture = originalRenderTexture; 86 | camera.aspect = originalAspect; 87 | } 88 | } 89 | 90 | private static void RenderTextureTo(RenderTexture renderTexture, Texture2D texture2D) 91 | { 92 | RenderTexture.active = renderTexture; 93 | texture2D.ReadPixels(new Rect(0, 0, renderTexture.width, renderTexture.height), 0, 0); 94 | texture2D.Apply(); 95 | RenderTexture.active = null; 96 | } 97 | 98 | public void Diff(Texture2D source, Texture2D neutralTexture, Texture2D newTexture) 99 | { 100 | var diff = RenderTexture.GetTemporary(newTexture.width, newTexture.height, 24); 101 | _material.SetTexture("_NeutralTex", neutralTexture); 102 | if (_useComputeShader) 103 | { 104 | _material.SetVector("_Rect", _diffCompute.Compute(source, neutralTexture)); 105 | } 106 | Graphics.Blit(source, diff, _material); 107 | RenderTextureTo(diff, newTexture); 108 | RenderTexture.ReleaseTemporary(diff); 109 | } 110 | } 111 | } -------------------------------------------------------------------------------- /Assets/Hai/VisualExpressionsEditor/Scripts/Editor/VisualExpressionsEditorGeneratorSingular.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 30d48b2047394b95bf9944d44c4ee524 3 | timeCreated: 1646711502 -------------------------------------------------------------------------------- /Assets/Hai/VisualExpressionsEditor/Scripts/Editor/VisualExpressionsEditorWindow.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.Immutable; 4 | using System.Globalization; 5 | using System.Linq; 6 | using System.Reflection; 7 | using UnityEditor; 8 | using UnityEngine; 9 | using Object = UnityEngine.Object; 10 | 11 | namespace Hai.VisualExpressionsEditor.Scripts.Editor 12 | { 13 | public class VisualExpressionsEditorWindow : EditorWindow 14 | { 15 | private const int MinWidth = 150; 16 | public Animator animator; 17 | public bool showDifferences = true; 18 | public bool autoUpdateOnFocus = false; 19 | public int thumbnailSize = 100; 20 | public bool showHotspots; 21 | public bool useComputeShader = true; 22 | public Texture2D[][] smrToTex2ds = new Texture2D[0][]; 23 | public bool animationLoopEdit = false; 24 | private Vector2 _scrollPos; 25 | private Animator _generatedFor; 26 | private int _generatedSize; 27 | 28 | private Vector3 _generatedTransformPosition; 29 | private Quaternion _generatedTransformRotation; 30 | private float _generatedFieldOfView; 31 | private bool _generatedOrthographic; 32 | private float _generatedNearClipPlane; 33 | private float _generatedFarClipPlane; 34 | private float _generatedOrthographicSize; 35 | private Rect m_area; 36 | private SkinnedMeshRenderer[] _allSkinnedMeshes; 37 | // 38 | 39 | public AnimationClip clip; 40 | public bool autoSelectClip = true; 41 | public bool advanced; 42 | public float normalizedTime; 43 | public HumanBodyBones focusedBone = HumanBodyBones.Head; 44 | public AnimationClip basePose; 45 | private Texture2D _clipTextureNullable; 46 | private HashSet _manuallyActedChanges = new HashSet(); 47 | private Rect m_ActiveRect; 48 | private Vector2 _scrollPosActive; 49 | 50 | private readonly Type _animationWindowType; 51 | private readonly FieldInfo _animationWindowAnimEditorField; 52 | private readonly FieldInfo _animEditorStateField; 53 | private readonly PropertyInfo _animationWindowStateCurrentFrameProperty; 54 | private readonly bool _loopEditFeatureAvailable; 55 | private int _lastCurrentFrame; 56 | private bool _isQuickFrame; 57 | private float _lastQuickFrame; 58 | private bool _isQuickPlaying; 59 | private float _isQuickPlayingTime; 60 | private float _isQuickPlayingFrame; 61 | private float _playSpeed = 1f; 62 | private bool _quickAnyways; 63 | 64 | public VisualExpressionsEditorWindow() 65 | { 66 | titleContent = new GUIContent("VisualExpressionsEditor"); 67 | 68 | try 69 | { 70 | _animationWindowType = typeof(EditorWindow).Assembly.GetType("UnityEditor.AnimationWindow"); 71 | _animationWindowAnimEditorField = _animationWindowType.GetField("m_AnimEditor", BindingFlags.NonPublic | BindingFlags.Instance); 72 | 73 | var animEditorType = typeof(EditorWindow).Assembly.GetType("UnityEditor.AnimEditor"); 74 | _animEditorStateField = animEditorType.GetField("m_State", BindingFlags.NonPublic | BindingFlags.Instance); 75 | 76 | var animationWindowStateType = typeof(EditorWindow).Assembly.GetType("UnityEditorInternal.AnimationWindowState"); 77 | _animationWindowStateCurrentFrameProperty = animationWindowStateType.GetProperty("currentFrame", BindingFlags.Public | BindingFlags.Instance); 78 | 79 | _loopEditFeatureAvailable = true; 80 | } 81 | catch (Exception e) 82 | { 83 | Debug.LogException(e); 84 | } 85 | } 86 | 87 | private void OnFocus() 88 | { 89 | if (!autoUpdateOnFocus) return; 90 | if (animator == null) return; 91 | if (!HasAnimatorGenerationParamsChanged()) return; 92 | 93 | TryExecuteAnimatorUpdate(); 94 | } 95 | 96 | private void OnInspectorUpdate() 97 | { 98 | if (!autoSelectClip) return; 99 | 100 | var active = Selection.activeObject; 101 | if (active == null) return; 102 | if (!(active is AnimationClip)) return; 103 | if (clip == active) return; 104 | 105 | clip = (AnimationClip) active; 106 | if (!_isQuickPlaying) 107 | { 108 | _isQuickFrame = false; 109 | _isQuickPlaying = false; 110 | } 111 | _manuallyActedChanges = new HashSet(); 112 | TryExecuteClipChangeUpdate(); 113 | } 114 | 115 | private void Update() 116 | { 117 | if (float.IsNaN(_lastQuickFrame)) 118 | { 119 | _lastQuickFrame = 0; 120 | } 121 | if (animationLoopEdit && _loopEditFeatureAvailable && _isQuickFrame && _isQuickPlaying && clip != null) 122 | { 123 | _lastQuickFrame = Mathf.Repeat(_isQuickPlayingFrame + (Time.time - _isQuickPlayingTime) * clip.frameRate * _playSpeed, Mathf.Max(1, clip.length * clip.frameRate)); 124 | TryExecuteClipChangeUpdate(); 125 | } 126 | } 127 | 128 | public void ChangeAnimator(Animator inAnimator) 129 | { 130 | animator = inAnimator; 131 | if (!_isQuickPlaying) 132 | { 133 | _isQuickFrame = false; 134 | _isQuickPlaying = false; 135 | } 136 | TryExecuteAnimatorUpdate(); 137 | TryExecuteClipChangeUpdate(); 138 | } 139 | 140 | public void ChangeClip(AnimationClip inClip) 141 | { 142 | if (clip == inClip) return; 143 | 144 | clip = inClip; 145 | if (!_isQuickPlaying) 146 | { 147 | _isQuickFrame = false; 148 | _isQuickPlaying = false; 149 | } 150 | _manuallyActedChanges = new HashSet(); 151 | TryExecuteClipChangeUpdate(); 152 | } 153 | 154 | private void OnGUI() 155 | { 156 | var serializedObject = new SerializedObject(this); 157 | EditorGUILayout.PropertyField(serializedObject.FindProperty(nameof(animator))); 158 | EditorGUILayout.BeginHorizontal(); 159 | EditorGUILayout.PropertyField(serializedObject.FindProperty(nameof(clip))); 160 | EditorGUILayout.LabelField("Auto Select", GUILayout.Width(100)); 161 | EditorGUILayout.PropertyField(serializedObject.FindProperty(nameof(autoSelectClip)), GUIContent.none, GUILayout.Width(25)); 162 | EditorGUILayout.EndHorizontal(); 163 | if (_loopEditFeatureAvailable) 164 | { 165 | EditorGUILayout.BeginHorizontal(); 166 | EditorGUILayout.IntSlider(serializedObject.FindProperty(nameof(thumbnailSize)), 100, 300); 167 | Colored(animationLoopEdit, Color.cyan, () => 168 | { 169 | EditorGUILayout.LabelField("Loop Edit", animationLoopEdit ? EditorStyles.boldLabel : EditorStyles.label, GUILayout.Width(100)); 170 | EditorGUILayout.PropertyField(serializedObject.FindProperty(nameof(animationLoopEdit)), GUIContent.none, GUILayout.Width(25)); 171 | }); 172 | EditorGUILayout.EndHorizontal(); 173 | } 174 | else 175 | { 176 | EditorGUILayout.IntSlider(serializedObject.FindProperty(nameof(thumbnailSize)), 100, 300); 177 | } 178 | 179 | EditorGUI.BeginDisabledGroup(animator == null || AnimationMode.InAnimationMode()); 180 | if (GUILayout.Button("Update")) 181 | { 182 | TryExecuteAnimatorUpdate(); 183 | _manuallyActedChanges = new HashSet(); 184 | TryExecuteClipChangeUpdate(); 185 | } 186 | EditorGUI.EndDisabledGroup(); 187 | // 188 | 189 | advanced = EditorGUILayout.Foldout(advanced, "Advanced"); 190 | if (advanced) 191 | { 192 | EditorGUILayout.BeginHorizontal(); 193 | EditorGUILayout.PropertyField(serializedObject.FindProperty(nameof(showDifferences))); 194 | if (showDifferences) 195 | { 196 | EditorGUILayout.PropertyField(serializedObject.FindProperty(nameof(showHotspots))); 197 | } 198 | EditorGUILayout.EndHorizontal(); 199 | EditorGUILayout.BeginHorizontal(); 200 | EditorGUILayout.PropertyField(serializedObject.FindProperty(nameof(autoUpdateOnFocus))); 201 | if (SystemInfo.supportsComputeShaders) 202 | { 203 | EditorGUILayout.PropertyField(serializedObject.FindProperty(nameof(useComputeShader))); 204 | } 205 | EditorGUILayout.EndHorizontal(); 206 | EditorGUILayout.PropertyField(serializedObject.FindProperty(nameof(focusedBone))); 207 | EditorGUILayout.Slider(serializedObject.FindProperty(nameof(normalizedTime)), 0f, 1f); 208 | EditorGUILayout.PropertyField(serializedObject.FindProperty(nameof(basePose))); 209 | } 210 | else 211 | { 212 | if (basePose != null) 213 | { 214 | EditorGUILayout.PropertyField(serializedObject.FindProperty(nameof(basePose))); 215 | } 216 | } 217 | 218 | if (basePose != null) 219 | { 220 | EditorGUILayout.HelpBox("A base pose is specified. This will change the way animations look.", MessageType.Warning); 221 | if (GUILayout.Button("Discard base pose")) 222 | { 223 | serializedObject.FindProperty(nameof(basePose)).objectReferenceValue = null; 224 | } 225 | } 226 | if (focusedBone != HumanBodyBones.Head) 227 | { 228 | if (GUILayout.Button("Reset focused bone to Head")) 229 | { 230 | serializedObject.FindProperty(nameof(focusedBone)).intValue = (int)HumanBodyBones.Head; 231 | } 232 | } 233 | 234 | serializedObject.ApplyModifiedProperties(); 235 | 236 | var isLoopEdit = animationLoopEdit && _loopEditFeatureAvailable; 237 | 238 | var width = Mathf.Max(_generatedSize, MinWidth); 239 | var showCondition = smrToTex2ds.Length > 0; 240 | if (animator != null && showCondition && _generatedFor == animator) 241 | { 242 | EditorGUILayout.BeginHorizontal(); 243 | EditorGUILayout.BeginVertical(GUILayout.Width(300)); 244 | GUILayout.Box(_clipTextureNullable); 245 | var all0ValueBindings = ImmutableHashSet.Empty; 246 | if (clip != null) 247 | { 248 | var bindings = AnimationUtility.GetCurveBindings(clip); 249 | all0ValueBindings = bindings 250 | .Where(binding => binding.type == typeof(SkinnedMeshRenderer) && binding.propertyName.StartsWith("blendShape.")) 251 | .Where(binding => AnimationUtility.GetEditorCurve(clip, binding).keys.All(keyframe => keyframe.value == 0f)) 252 | .ToImmutableHashSet(); 253 | if (all0ValueBindings.Count > 0) 254 | { 255 | EditorGUILayout.BeginHorizontal(); 256 | if (ColoredBgButton(true, Color.yellow, () => GUILayout.Button($"Delete all 0-values ({all0ValueBindings.Count})", GUILayout.Width(300)))) 257 | { 258 | Undo.SetCurrentGroupName($"VEE Remove {all0ValueBindings.Count} 0-values"); 259 | foreach (var binding in all0ValueBindings) 260 | { 261 | RemoveAnimatable(binding, false); 262 | } 263 | TryExecuteClipChangeUpdate(); 264 | } 265 | EditorGUILayout.EndHorizontal(); 266 | } 267 | else 268 | { 269 | EditorGUILayout.BeginHorizontal(); 270 | EditorGUI.BeginDisabledGroup(true); 271 | ColoredBgButton(true, Color.yellow, () => GUILayout.Button($"Delete all 0-values ({all0ValueBindings.Count})", GUILayout.Width(0))); 272 | EditorGUI.EndDisabledGroup(); 273 | EditorGUILayout.EndHorizontal(); 274 | } 275 | 276 | if (isLoopEdit) 277 | { 278 | var newLooping = EditorGUILayout.Toggle("Is Looping Animation", clip.isLooping); 279 | if (newLooping != clip.isLooping) 280 | { 281 | Undo.RecordObject(clip, "VEE Change clip looping"); 282 | var settings = AnimationUtility.GetAnimationClipSettings(clip); 283 | settings.loopTime = newLooping; 284 | AnimationUtility.SetAnimationClipSettings(clip, settings); 285 | } 286 | if (!clip.isLooping) 287 | { 288 | EditorGUILayout.HelpBox("This animation clip is not set to looping.", MessageType.Error); 289 | } 290 | } 291 | } 292 | EditorGUILayout.EndVertical(); 293 | var height = isLoopEdit ? 470 : 350; 294 | RectOnRepaint(() => GUILayoutUtility.GetRect(width - 300, height), rect => m_ActiveRect = rect); 295 | GUILayout.BeginArea(m_ActiveRect); 296 | if (clip != null && isLoopEdit) 297 | { 298 | LoopEditMode(); 299 | } 300 | 301 | if (isLoopEdit && _isQuickFrame) 302 | { 303 | if (_isQuickPlaying) 304 | { 305 | _quickAnyways = EditorGUILayout.Toggle("Edit during play (SLOW)", _quickAnyways); 306 | } 307 | if (!_isQuickPlaying || !_quickAnyways) 308 | { 309 | if (GUILayout.Button("Continue...", GUILayout.Height(100))) 310 | { 311 | _isQuickFrame = false; 312 | _isQuickPlaying = false; 313 | } 314 | GUILayout.EndArea(); 315 | EditorGUILayout.EndHorizontal(); 316 | return; 317 | } 318 | } 319 | 320 | _scrollPosActive = GUILayout.BeginScrollView(_scrollPosActive, GUILayout.Height(height - (isLoopEdit ? 70 : 0))); 321 | DisplayBlendshapeSelector(width, (int)position.width - 300, all0ValueBindings, true); 322 | GUILayout.EndScrollView(); 323 | GUILayout.EndArea(); 324 | EditorGUILayout.EndHorizontal(); 325 | 326 | _scrollPos = GUILayout.BeginScrollView(_scrollPos, GUILayout.Height(position.height - EditorGUIUtility.singleLineHeight * ( 327 | 7 328 | + (advanced ? 6 : 0) 329 | + (basePose != null ? (advanced ? 3 : 5) : 0) 330 | + (focusedBone != HumanBodyBones.Head ? 1 : 0) 331 | ) - height)); 332 | DisplayBlendshapeSelector(width, (int)position.width, all0ValueBindings, false); 333 | GUILayout.EndScrollView(); 334 | } 335 | } 336 | 337 | private void LoopEditMode() 338 | { 339 | // EditorGUILayout.HelpBox("Loop Edit mode is active. When in this mode, the sliders will behave differently.\nDisable Loop Edit mode if you are not editing looping clips.", MessageType.Warning); 340 | var currentFrame = ReflectiveGetFirstAnimationTabFrame(); 341 | var totalLength = Mathf.Max(1, (int) (clip.frameRate * clip.length)); 342 | 343 | EditorGUILayout.BeginHorizontal(); 344 | var quickFrame = ColoredReturning(_isQuickFrame, Color.cyan, () => EditorGUILayout.Slider("Quick Preview", _lastQuickFrame, 0, totalLength)); 345 | if (ColoredBgButton(_isQuickPlaying, Color.green, () => GUILayout.Button("Play", GUILayout.Width(50)))) 346 | { 347 | if (!_isQuickPlaying) 348 | { 349 | _isQuickFrame = true; 350 | _isQuickPlaying = true; 351 | _isQuickPlayingFrame = _lastQuickFrame; 352 | _isQuickPlayingTime = Time.time; 353 | } 354 | else 355 | { 356 | _isQuickFrame = false; 357 | _isQuickPlaying = false; 358 | } 359 | } 360 | EditorGUILayout.EndHorizontal(); 361 | 362 | EditorGUILayout.BeginHorizontal(); 363 | var sliderInfluencedFrame = EditorGUILayout.IntSlider("Edit Frame", currentFrame, 0, totalLength); 364 | if (_isQuickPlaying) 365 | { 366 | _playSpeed = EditorGUILayout.FloatField(_playSpeed, GUILayout.Width(50)); 367 | } 368 | else 369 | { 370 | EditorGUILayout.LabelField("", GUILayout.Width(50)); 371 | } 372 | EditorGUILayout.EndHorizontal(); 373 | 374 | if (_lastCurrentFrame <= totalLength && quickFrame != _lastQuickFrame) 375 | { 376 | _isQuickFrame = true; 377 | } 378 | else if (currentFrame <= totalLength && sliderInfluencedFrame != currentFrame) 379 | { 380 | ReflectiveSetFirstAnimationTabFrame(sliderInfluencedFrame); 381 | GetWindow(_animationWindowType, false, null, false).Repaint(); 382 | _isQuickFrame = false; 383 | _isQuickPlaying = false; 384 | currentFrame = sliderInfluencedFrame; 385 | } 386 | 387 | if (currentFrame != _lastCurrentFrame || _isQuickFrame && quickFrame != _lastQuickFrame) 388 | { 389 | TryExecuteClipChangeUpdate(); 390 | } 391 | 392 | _lastQuickFrame = !_isQuickFrame ? currentFrame : quickFrame; 393 | _lastCurrentFrame = currentFrame; 394 | } 395 | 396 | private float CurrentQuickFrameOrAnimationTabFrame() 397 | { 398 | if (_isQuickFrame) return _lastQuickFrame; 399 | return ReflectiveGetFirstAnimationTabFrame(); 400 | } 401 | 402 | private int ReflectiveGetFirstAnimationTabFrame() 403 | { 404 | var animationWindow = GetWindow(_animationWindowType, false, null, false); 405 | var animEditor = _animationWindowAnimEditorField.GetValue(animationWindow); 406 | var editorState = _animEditorStateField.GetValue(animEditor); 407 | var currentFrame = (int) _animationWindowStateCurrentFrameProperty.GetValue(editorState); 408 | return currentFrame; 409 | } 410 | 411 | private void ReflectiveSetFirstAnimationTabFrame(int frame) 412 | { 413 | var animationWindow = GetWindow(_animationWindowType, false, null, false); 414 | var animEditor = _animationWindowAnimEditorField.GetValue(animationWindow); 415 | var editorState = _animEditorStateField.GetValue(animEditor); 416 | _animationWindowStateCurrentFrameProperty.SetValue(editorState, frame); 417 | } 418 | 419 | public static void RectOnRepaint(Func rectFn, Action applyFn) 420 | { 421 | var rect = rectFn(); 422 | if (Event.current.type == EventType.Repaint) 423 | { 424 | // https://answers.unity.com/questions/515197/how-to-use-guilayoututilitygetrect-properly.html 425 | applyFn(rect); 426 | } 427 | } 428 | 429 | private void DisplayBlendshapeSelector(int width, int screenWidth, ImmutableHashSet all0ValueBindings, bool summaryView) 430 | { 431 | var mod = Mathf.Max(1, screenWidth / (width + 15)); 432 | var highlightColor = EditorGUIUtility.isProSkin ? new Color(0.92f, 0.62f, 0.25f) : new Color(0.74f, 0.47f, 0.1f); 433 | 434 | var isLoopEdit = animationLoopEdit && _loopEditFeatureAvailable; 435 | var currentFrame = isLoopEdit ? ReflectiveGetFirstAnimationTabFrame() : 0; 436 | var currentTime = (currentFrame * 1f) / (clip != null ? clip.frameRate : 60); 437 | 438 | var allBindings = clip != null ? AnimationUtility.GetCurveBindings(clip).ToImmutableHashSet() : ImmutableHashSet.Empty; 439 | var layoutActive = false; 440 | var layoutCounter = 0; 441 | for (var smrIndex = 0; smrIndex < _allSkinnedMeshes.Length; smrIndex++) 442 | { 443 | var skinnedMesh = _allSkinnedMeshes[smrIndex]; 444 | var tex2ds = smrToTex2ds[smrIndex]; 445 | if (!summaryView) 446 | { 447 | EditorGUI.BeginDisabledGroup(true); 448 | EditorGUILayout.ObjectField(skinnedMesh.name, skinnedMesh, typeof(SkinnedMeshRenderer), EditorStyles.boldFont); 449 | EditorGUI.EndDisabledGroup(); 450 | } 451 | var total = tex2ds.Length; 452 | 453 | var transformPath = AnimationUtility.CalculateTransformPath(skinnedMesh.transform, animator.transform); 454 | if (!summaryView) 455 | { 456 | layoutCounter = 0; 457 | } 458 | for (var index = 0; index < total; index++) 459 | { 460 | var blendShapeName = skinnedMesh.sharedMesh.GetBlendShapeName(index); 461 | 462 | var binding = new EditorCurveBinding 463 | { 464 | path = transformPath, 465 | propertyName = $"blendShape.{blendShapeName}", 466 | type = typeof(SkinnedMeshRenderer) 467 | }; 468 | var existsInAnimation = allBindings.Contains(binding); 469 | var existsFirstValue = -1f; 470 | var existsAllSameValue = false; 471 | if (existsInAnimation) 472 | { 473 | if (all0ValueBindings.Contains(binding)) 474 | { 475 | existsFirstValue = 0f; 476 | existsAllSameValue = true; 477 | } 478 | else 479 | { 480 | var curve = AnimationUtility.GetEditorCurve(clip, binding); 481 | existsFirstValue = curve.keys.Length > 0 ? curve.keys[0].value : -1f; // Defensive 482 | existsAllSameValue = curve.keys.All(keyframe => keyframe.value == existsFirstValue); 483 | } 484 | } 485 | 486 | if (summaryView && (!existsInAnimation || existsAllSameValue && existsFirstValue == 0 && !_manuallyActedChanges.Contains(binding))) 487 | { 488 | continue; 489 | } 490 | 491 | var texture2D = tex2ds[index]; 492 | if (layoutCounter % mod == 0) 493 | { 494 | layoutActive = true; 495 | EditorGUILayout.BeginHorizontal(); 496 | GUILayout.FlexibleSpace(); 497 | } 498 | 499 | EditorGUILayout.BeginVertical(); 500 | if (!summaryView) 501 | { 502 | if (ColoredBgButton(existsInAnimation, existsAllSameValue && existsFirstValue == 0f ? Color.yellow : Color.red, () => GUILayout.Button(texture2D, GUILayout.Width(texture2D.width), GUILayout.Height(texture2D.height)))) 503 | { 504 | if (existsInAnimation && (!existsAllSameValue || existsFirstValue != 0)) 505 | { 506 | RemoveAnimatable(binding); 507 | } 508 | else 509 | { 510 | AddAnimatable(binding); 511 | } 512 | } 513 | } 514 | else 515 | { 516 | GUILayout.Box(texture2D); 517 | } 518 | 519 | if (summaryView || existsInAnimation && existsAllSameValue && existsFirstValue == 0f) 520 | { 521 | EditorGUILayout.BeginHorizontal(); 522 | Colored(existsInAnimation, highlightColor, () => { EditorGUILayout.TextField(blendShapeName, existsInAnimation ? EditorStyles.boldLabel : EditorStyles.label, GUILayout.Width(width - 25)); }); 523 | if (ColoredBgButton(true, Color.red, () => GUILayout.Button("×", GUILayout.Width(20)))) 524 | { 525 | RemoveAnimatable(binding); 526 | } 527 | EditorGUILayout.EndHorizontal(); 528 | } 529 | else 530 | { 531 | Colored(existsInAnimation, highlightColor, () => { EditorGUILayout.TextField(blendShapeName, existsInAnimation ? EditorStyles.boldLabel : EditorStyles.label, GUILayout.Width(width)); }); 532 | } 533 | 534 | // var weight = serializedSkinnedMesh.FindProperty("m_BlendShapeWeights").GetArrayElementAtIndex(index); 535 | // var isNonZero = weight.floatValue > 0f; 536 | if (existsInAnimation) 537 | { 538 | if (isLoopEdit) 539 | { 540 | var found = false; 541 | var isFirstOrLastKey = false; 542 | var foundKeyIndex = 0; 543 | 544 | var curve = AnimationUtility.GetEditorCurve(clip, binding); 545 | if (curve.keys.Length == 1 || (curve.keys.Length == 2 && curve.keys[0].value == curve.keys[1].value)) 546 | { 547 | var oldValue = curve.keys[0].value; 548 | var newValue = ColoredReturning(true, Color.cyan, () => EditorGUILayout.Slider(GUIContent.none, oldValue, Mathf.Min(0f, oldValue), Mathf.Max(100f, oldValue), GUILayout.Width(width))); 549 | if (newValue != oldValue) 550 | { 551 | _manuallyActedChanges.Add(binding); 552 | ModifyAnimatable(binding, newValue); 553 | } 554 | 555 | found = Math.Abs(currentTime - curve.keys[0].time) < 0.001f || curve.keys.Length == 2 && Math.Abs(currentTime - curve.keys[1].time) < 0.001f; 556 | isFirstOrLastKey = found; 557 | } 558 | else 559 | { 560 | for (var keyIndex = 0; keyIndex < curve.keys.Length; keyIndex++) 561 | { 562 | var curveKey = curve.keys[keyIndex]; 563 | if (curveKey.time == currentTime) 564 | { 565 | isFirstOrLastKey = keyIndex == 0 || keyIndex == curve.keys.Length - 1; 566 | foundKeyIndex = keyIndex; 567 | found = true; 568 | break; 569 | } 570 | } 571 | 572 | if (found) 573 | { 574 | var foundCurveKey = curve.keys[foundKeyIndex]; 575 | var isSameValueInOtherEnd = false; 576 | var oldValue = foundCurveKey.value; 577 | if (isFirstOrLastKey) 578 | { 579 | var otherEndKeyIndex = foundKeyIndex == 0 ? curve.keys.Length - 1 : 0; 580 | var otherEndCurveKey = curve.keys[otherEndKeyIndex]; 581 | isSameValueInOtherEnd = otherEndCurveKey.value == oldValue; 582 | } 583 | 584 | if (isSameValueInOtherEnd) 585 | { 586 | var newValue = ColoredReturning(true, Color.cyan, () => EditorGUILayout.Slider(GUIContent.none, oldValue, Mathf.Min(0f, oldValue), Mathf.Max(100f, oldValue), GUILayout.Width(width))); 587 | if (newValue != oldValue) 588 | { 589 | ModifyMultipleKeyframeValueAnimatable(binding, 0, curve.keys.Length - 1, newValue); 590 | } 591 | } 592 | else 593 | { 594 | var newValue = EditorGUILayout.Slider(GUIContent.none, oldValue, Mathf.Min(0f, oldValue), Mathf.Max(100f, oldValue), GUILayout.Width(width)); 595 | if (newValue != oldValue) 596 | { 597 | ModifyKeyframeValueAnimatable(binding, foundKeyIndex, newValue); 598 | } 599 | } 600 | } 601 | 602 | if (!found) 603 | { 604 | EditorGUI.BeginDisabledGroup(true); 605 | var oldValue = curve.Evaluate(currentTime); 606 | EditorGUILayout.Slider(GUIContent.none, oldValue, Mathf.Min(0f, oldValue), Mathf.Max(100f, oldValue), GUILayout.Width(width)); 607 | EditorGUI.EndDisabledGroup(); 608 | } 609 | } 610 | 611 | var isLoopingBroken = curve.keys.Length >= 2 && curve.keys[0].value != curve.keys[curve.keys.Length - 1].value; 612 | 613 | EditorGUILayout.BeginHorizontal(); 614 | // EditorGUILayout.TextField($"×{curve.length}", false ? EditorStyles.boldLabel : EditorStyles.label, GUILayout.Width(20)); 615 | 616 | EditorGUI.BeginDisabledGroup(!curve.keys.Any(keyframe => keyframe.time < currentTime - 0.001f)); 617 | if (GUILayout.Button("<", GUILayout.Width(20))) 618 | { 619 | ReflectiveSetFirstAnimationTabFrame(Mathf.RoundToInt(curve.keys.Select(keyframe => keyframe.time).Last(time => time < currentTime - 0.001f) * clip.frameRate)); 620 | } 621 | EditorGUI.EndDisabledGroup(); 622 | 623 | EditorGUI.BeginDisabledGroup(isFirstOrLastKey); 624 | if (found && !isFirstOrLastKey) 625 | { 626 | if (ColoredBgButton(true, Color.red, () => GUILayout.Button("×", GUILayout.Width(40)))) 627 | { 628 | ModifyRemoveKeyframeAnimatable(binding, foundKeyIndex); 629 | } 630 | } 631 | else 632 | { 633 | if (ColoredBgButton(true, Color.cyan, () => GUILayout.Button("+", GUILayout.Width(40)))) 634 | { 635 | ModifyAddKeyframeAnimatable(binding, curve.Evaluate(currentTime), currentTime); 636 | } 637 | } 638 | EditorGUI.EndDisabledGroup(); 639 | 640 | EditorGUI.BeginDisabledGroup(!curve.keys.Any(keyframe => keyframe.time > currentTime + 0.001f)); 641 | if (GUILayout.Button(">", GUILayout.Width(20))) 642 | { 643 | ReflectiveSetFirstAnimationTabFrame(Mathf.RoundToInt(curve.keys.Select(keyframe => keyframe.time).First(time => time > currentTime + 0.001f) * clip.frameRate)); 644 | } 645 | EditorGUI.EndDisabledGroup(); 646 | 647 | EditorGUI.BeginDisabledGroup(true); 648 | if (ColoredBgButton(true, isLoopingBroken ? Color.magenta : Color.cyan, () => GUILayout.Button(isLoopingBroken ? "Broken" : "", GUILayout.Width(isLoopingBroken ? 50 : 0)))) 649 | { 650 | } 651 | EditorGUI.EndDisabledGroup(); 652 | EditorGUILayout.EndHorizontal(); 653 | } 654 | else 655 | { 656 | if (!existsAllSameValue) 657 | { 658 | var curve = AnimationUtility.GetEditorCurve(clip, binding); 659 | var min = curve.keys.Select(keyframe => keyframe.value).Min(); 660 | var max = curve.keys.Select(keyframe => keyframe.value).Max(); 661 | EditorGUILayout.LabelField(string.Format(CultureInfo.InvariantCulture, "[{0:0.##} : {1:0.##}]", min, max), GUILayout.Width(width)); 662 | } 663 | else if (existsAllSameValue && (existsFirstValue < 0f || existsFirstValue > 100f)) 664 | { 665 | EditorGUILayout.LabelField(string.Format(CultureInfo.InvariantCulture, "[{0:0.##}]", existsFirstValue), GUILayout.Width(width)); 666 | } 667 | else if ((existsFirstValue != 0f || _manuallyActedChanges.Contains(binding)) 668 | // Some animations have constants outside threshold, and these will get silently edited to 100 if the editor is allowed 669 | && existsFirstValue >= 0f && existsFirstValue <= 100f) 670 | { 671 | var newValue = EditorGUILayout.Slider(GUIContent.none, existsFirstValue, 0f, 100f, GUILayout.Width(width)); 672 | if (newValue != existsFirstValue) 673 | { 674 | _manuallyActedChanges.Add(binding); 675 | ModifyAnimatable(binding, newValue); 676 | } 677 | } 678 | } 679 | } 680 | 681 | EditorGUILayout.EndVertical(); 682 | 683 | if ((layoutCounter + 1) % mod == 0 || (!summaryView && layoutCounter == total - 1)) 684 | { 685 | GUILayout.FlexibleSpace(); 686 | EditorGUILayout.EndHorizontal(); 687 | layoutActive = false; 688 | } 689 | 690 | layoutCounter++; 691 | } 692 | } 693 | 694 | if (summaryView && layoutActive) 695 | { 696 | GUILayout.FlexibleSpace(); 697 | EditorGUILayout.EndHorizontal(); 698 | } 699 | } 700 | 701 | private void AddAnimatable(EditorCurveBinding binding) 702 | { 703 | if (clip == null) return; 704 | Undo.RecordObject(clip, $"VEE Add {binding.propertyName} at {binding.path}"); 705 | var max = animationLoopEdit && _loopEditFeatureAvailable && AnimationUtility.GetCurveBindings(clip).Length > 0 ? Mathf.Max(1 / clip.frameRate, clip.length) : 1/60f; 706 | AnimationUtility.SetEditorCurve(clip, binding, AnimationCurve.Constant(0, max, 100f)); 707 | TryExecuteClipChangeUpdate(); 708 | } 709 | 710 | private void RemoveAnimatable(EditorCurveBinding binding, bool update = true) 711 | { 712 | if (clip == null) return; 713 | Undo.RecordObject(clip, $"VEE Remove {binding.propertyName} at {binding.path}"); 714 | AnimationUtility.SetEditorCurve(clip, binding, null); 715 | if (update) 716 | { 717 | TryExecuteClipChangeUpdate(); 718 | } 719 | } 720 | 721 | private void ModifyAnimatable(EditorCurveBinding binding, float newValue) 722 | { 723 | if (clip == null) return; 724 | Undo.RecordObject(clip, $"VEE Change {binding.propertyName} at {binding.path}"); 725 | var curve = AnimationUtility.GetEditorCurve(clip, binding); 726 | curve.keys = curve.keys.Select(keyframe => 727 | { 728 | keyframe.value = newValue; 729 | return keyframe; 730 | }).ToArray(); 731 | AnimationUtility.SetEditorCurve(clip, binding, curve); 732 | TryExecuteClipChangeUpdate(); 733 | } 734 | 735 | private void ModifyAddKeyframeAnimatable(EditorCurveBinding binding, float newValue, float currentTime) 736 | { 737 | if (clip == null) return; 738 | Undo.RecordObject(clip, $"VEE Add keyframe {binding.propertyName} at {binding.path}"); 739 | var curve = AnimationUtility.GetEditorCurve(clip, binding); 740 | 741 | // The following line needs to be executed before adding the key 742 | var existingAreTwoIdentical = curve.keys.Length == 2 && curve.keys[0].value == curve.keys[1].value; 743 | // Order matters here 744 | var addedIndex = curve.AddKey(currentTime, newValue); 745 | 746 | if (existingAreTwoIdentical) 747 | { 748 | // By default, created curves is constant pair of identical keyframes. 749 | // If the existing two were identical, turn these curves into clamped auto curves, so that adding new keyframes and then modifying the value 750 | // won't cause issues with the tangent not updating. 751 | for (var i = 0; i < 3; i++) 752 | { 753 | TrySetTangentOnly(curve, i); 754 | } 755 | TryUpdateTangents(curve); 756 | } 757 | else 758 | { 759 | TrySetTangentMode(curve, addedIndex); 760 | } 761 | AnimationUtility.SetEditorCurve(clip, binding, curve); 762 | TryExecuteClipChangeUpdate(); 763 | } 764 | 765 | private void ModifyRemoveKeyframeAnimatable(EditorCurveBinding binding, int index) 766 | { 767 | if (clip == null) return; 768 | Undo.RecordObject(clip, $"VEE Remove keyframe {binding.propertyName} at {binding.path}"); 769 | var curve = AnimationUtility.GetEditorCurve(clip, binding); 770 | curve.RemoveKey(index); 771 | TryUpdateTangents(curve); 772 | AnimationUtility.SetEditorCurve(clip, binding, curve); 773 | TryExecuteClipChangeUpdate(); 774 | } 775 | 776 | private void ModifyKeyframeValueAnimatable(EditorCurveBinding binding, int index, float newValue) 777 | { 778 | if (clip == null) return; 779 | Undo.RecordObject(clip, $"VEE Change keyframe {binding.propertyName} at {binding.path}"); 780 | var curve = AnimationUtility.GetEditorCurve(clip, binding); 781 | var keys = curve.keys; 782 | keys[index].value = newValue; 783 | curve.keys = keys; 784 | // TrySetTangentMode(curve, index); 785 | TryUpdateTangents(curve); 786 | AnimationUtility.SetEditorCurve(clip, binding, curve); 787 | TryExecuteClipChangeUpdate(); 788 | } 789 | 790 | private void ModifyMultipleKeyframeValueAnimatable(EditorCurveBinding binding, int indexA, int indexB, float newValue) 791 | { 792 | if (clip == null) return; 793 | Undo.RecordObject(clip, $"VEE Change keyframe {binding.propertyName} at {binding.path}"); 794 | var curve = AnimationUtility.GetEditorCurve(clip, binding); 795 | var keys = curve.keys; 796 | keys[indexA].value = newValue; 797 | keys[indexB].value = newValue; 798 | // TrySetTangentMode(curve, indexA); 799 | // TrySetTangentMode(curve, indexB); 800 | TryUpdateTangents(curve); 801 | curve.keys = keys; 802 | AnimationUtility.SetEditorCurve(clip, binding, curve); 803 | TryExecuteClipChangeUpdate(); 804 | } 805 | 806 | private void TryUpdateTangents(AnimationCurve curve) 807 | { 808 | typeof(AnimationUtility) 809 | .GetMethod("UpdateTangentsFromMode", BindingFlags.NonPublic | BindingFlags.Static) 810 | .Invoke(null, new object[] {curve}); 811 | } 812 | 813 | private static void TrySetTangentMode(AnimationCurve curve, int index) 814 | { 815 | TrySetTangentOnly(curve, index); 816 | // AnimationUtility.UpdateTangentsFromModeSurrounding(curve, index); 817 | // CurveUtility.SetKeyModeFromContext(curve, index); 818 | typeof(AnimationUtility) 819 | .GetMethod("UpdateTangentsFromModeSurrounding", BindingFlags.NonPublic | BindingFlags.Static) 820 | .Invoke(null, new object[] {curve, index}); 821 | typeof(AnimationUtility).Assembly.GetType("UnityEditor.CurveUtility") 822 | .GetMethod("SetKeyModeFromContext", BindingFlags.Public | BindingFlags.Static) 823 | .Invoke(null, new object[] {curve, index}); 824 | } 825 | 826 | private static void TrySetTangentOnly(AnimationCurve curve, int index) 827 | { 828 | AnimationUtility.SetKeyLeftTangentMode(curve, index, AnimationUtility.TangentMode.ClampedAuto); 829 | AnimationUtility.SetKeyRightTangentMode(curve, index, AnimationUtility.TangentMode.ClampedAuto); 830 | } 831 | 832 | public void TryExecuteClipChangeUpdate() 833 | { 834 | if (AnimationMode.InAnimationMode()) return; 835 | 836 | GenerateClip(); 837 | } 838 | 839 | public void TryExecuteAnimatorUpdate() 840 | { 841 | if (AnimationMode.InAnimationMode()) return; 842 | 843 | _allSkinnedMeshes = animator.GetComponentsInChildren(true) 844 | .Where(renderer => renderer.sharedMesh != null && renderer.sharedMesh.blendShapeCount > 0) 845 | .OrderByDescending(renderer => renderer.sharedMesh.blendShapeCount) 846 | .ToArray(); 847 | smrToTex2ds = new Texture2D[_allSkinnedMeshes.Length][]; 848 | for (var index = 0; index < _allSkinnedMeshes.Length; index++) 849 | { 850 | var smr = _allSkinnedMeshes[index]; 851 | smrToTex2ds[index] = GenerateBlendShapes(smr); 852 | } 853 | 854 | SaveAnimatorGenerationParams(); 855 | } 856 | 857 | private void SaveAnimatorGenerationParams() 858 | { 859 | _generatedFor = animator; 860 | _generatedSize = thumbnailSize; 861 | 862 | var sceneCamera = SceneView.lastActiveSceneView.camera; 863 | _generatedTransformPosition = sceneCamera.transform.position; 864 | _generatedTransformRotation = sceneCamera.transform.rotation; 865 | var whRatio = (1f * sceneCamera.pixelWidth / sceneCamera.pixelHeight); 866 | _generatedFieldOfView = whRatio < 1 ? sceneCamera.fieldOfView * whRatio : sceneCamera.fieldOfView; 867 | _generatedOrthographic = sceneCamera.orthographic; 868 | _generatedNearClipPlane = sceneCamera.nearClipPlane; 869 | _generatedFarClipPlane = sceneCamera.farClipPlane; 870 | _generatedOrthographicSize = sceneCamera.orthographicSize; 871 | } 872 | 873 | private bool HasAnimatorGenerationParamsChanged() 874 | { 875 | var sceneCamera = SceneView.lastActiveSceneView.camera; 876 | if (_generatedTransformPosition != sceneCamera.transform.position) return true; 877 | if (_generatedTransformRotation != sceneCamera.transform.rotation) return true; 878 | var whRatio = (1f * sceneCamera.pixelWidth / sceneCamera.pixelHeight); 879 | if (Math.Abs(_generatedFieldOfView - (whRatio < 1 ? sceneCamera.fieldOfView * whRatio : sceneCamera.fieldOfView)) > 0.001f) return true; 880 | if (_generatedOrthographic != sceneCamera.orthographic) return true; 881 | if (Math.Abs(_generatedNearClipPlane - sceneCamera.nearClipPlane) > 0.001f) return true; 882 | if (Math.Abs(_generatedFarClipPlane - sceneCamera.farClipPlane) > 0.001f) return true; 883 | if (Math.Abs(_generatedOrthographicSize - sceneCamera.orthographicSize) > 0.001f) return true; 884 | return false; 885 | } 886 | 887 | private void UsingAnimator(Animator inAnimator) 888 | { 889 | animator = inAnimator; 890 | } 891 | 892 | private void GenerateClip() 893 | { 894 | TryRender(animator.gameObject); 895 | } 896 | 897 | private bool TryRender(GameObject root) 898 | { 899 | var originalAvatarGo = root; 900 | GameObject copy = null; 901 | var wasActive = originalAvatarGo.activeSelf; 902 | try 903 | { 904 | copy = Object.Instantiate(originalAvatarGo); 905 | copy.SetActive(true); 906 | originalAvatarGo.SetActive(false); 907 | Render(copy); 908 | } 909 | finally 910 | { 911 | if (wasActive) originalAvatarGo.SetActive(true); 912 | if (copy != null) Object.DestroyImmediate(copy); 913 | } 914 | 915 | return true; 916 | } 917 | 918 | 919 | private void Render(GameObject copy) 920 | { 921 | var viewer = new VisualExpressionsEditorGeneratorClip(); 922 | try 923 | { 924 | viewer.Begin(copy); 925 | var animator = copy.GetComponent(); 926 | if (animator.isHuman && focusedBone != HumanBodyBones.LastBone) 927 | { 928 | var head = animator.GetBoneTransform(focusedBone); 929 | viewer.ParentCameraTo(head); 930 | } 931 | else 932 | { 933 | viewer.ParentCameraTo(animator.transform); 934 | } 935 | 936 | var texture = new Texture2D(300, 300, TextureFormat.RGB24, true); 937 | 938 | var itemCount = 0; 939 | var localClip = clip; 940 | if (localClip == null) localClip = new AnimationClip(); // Defensive: Might happen if the clip gets deleted during an update 941 | var renderTime = animationLoopEdit && _loopEditFeatureAvailable && clip.length > 0f 942 | ? (CurrentQuickFrameOrAnimationTabFrame() * 1f / (clip.length * clip.frameRate)) 943 | : normalizedTime; 944 | if (basePose != null) 945 | { 946 | var modifiedClip = Object.Instantiate(localClip); 947 | var missingBindings = AnimationUtility.GetCurveBindings(basePose) 948 | .Where(binding => AnimationUtility.GetEditorCurve(localClip, binding) == null) 949 | .ToArray(); 950 | foreach (var missingBinding in missingBindings) 951 | { 952 | AnimationUtility.SetEditorCurve(modifiedClip, missingBinding, AnimationUtility.GetEditorCurve(basePose, missingBinding)); 953 | } 954 | viewer.Render(modifiedClip, texture, renderTime); 955 | } 956 | else 957 | { 958 | viewer.Render(localClip, texture, renderTime); 959 | } 960 | 961 | _clipTextureNullable = texture; 962 | 963 | // Warning: This is from the "Animation Viewer" project. 964 | // This may not apply here since there is no queue; the animator copy is only consumed once. 965 | // [[ 966 | // This is a workaround for an issue where the muscles will not update 967 | // across multiple samplings of the animator on the same frame. 968 | // This issue is mainly visible when the update speed (number of animation 969 | // clips updated per frame) is greater than 1. 970 | // By disabling and enabling the animator copy, this allows us to resample it. 971 | copy.SetActive(false); 972 | copy.SetActive(true); 973 | // ]] 974 | } 975 | finally 976 | { 977 | viewer.Terminate(); 978 | } 979 | } 980 | 981 | private Texture2D[] GenerateBlendShapes(SkinnedMeshRenderer skinnedMesh) 982 | { 983 | var module = new VisualExpressionsEditorGeneratorSingular(); 984 | try 985 | { 986 | module.Begin(skinnedMesh.gameObject, showHotspots ? 0.95f : 0, useComputeShader); 987 | Texture2D neutralTexture = null; 988 | if (showDifferences) 989 | { 990 | neutralTexture = NewTexture(); 991 | module.Render(EmptyClip(), neutralTexture); 992 | } 993 | 994 | var results = new [] {skinnedMesh} 995 | .SelectMany(relevantSmr => 996 | { 997 | var sharedMesh = relevantSmr.sharedMesh; 998 | 999 | return Enumerable.Range(0, sharedMesh.blendShapeCount) 1000 | .Select(i => 1001 | { 1002 | var blendShapeName = sharedMesh.GetBlendShapeName(i); 1003 | var currentWeight = relevantSmr.GetBlendShapeWeight(i); 1004 | 1005 | // If the user has already animated this to 100, in normal circumstances the diff would show nothing. 1006 | // Animate the blendshape to 0 instead so that a diff can be generated. 1007 | var isAlreadyAnimatedTo100 = Math.Abs(currentWeight - 100f) < 0.001f; 1008 | var tempClip = new AnimationClip(); 1009 | AnimationUtility.SetEditorCurve( 1010 | tempClip, 1011 | new EditorCurveBinding 1012 | { 1013 | path = "", 1014 | type = typeof(SkinnedMeshRenderer), 1015 | propertyName = $"blendShape.{blendShapeName}" 1016 | }, 1017 | AnimationCurve.Constant(0, 1 / 60f, isAlreadyAnimatedTo100 ? 0f : 100f) 1018 | ); 1019 | 1020 | return tempClip; 1021 | }) 1022 | .ToArray(); 1023 | }) 1024 | .ToArray(); 1025 | 1026 | return results 1027 | .Select((clip, i) => 1028 | { 1029 | if (i % 10 == 0) EditorUtility.DisplayProgressBar("Rendering", $"Rendering ({i} / {results.Length})", 1f * i / results.Length); 1030 | 1031 | var currentWeight = skinnedMesh.GetBlendShapeWeight(i); 1032 | var isAlreadyAnimatedTo100 = Math.Abs(currentWeight - 100f) < 0.001f; 1033 | 1034 | var result = NewTexture(); 1035 | module.Render(clip, result); 1036 | if (i == 0) 1037 | { 1038 | // Workaround a weird bug where the first blendshape is always incorrectly rendered 1039 | module.Render(clip, result); 1040 | } 1041 | if (showDifferences) 1042 | { 1043 | if (isAlreadyAnimatedTo100) 1044 | { 1045 | module.Diff(neutralTexture, result, result); 1046 | } 1047 | else 1048 | { 1049 | module.Diff(result, neutralTexture, result); 1050 | } 1051 | } 1052 | return result; 1053 | }) 1054 | .ToArray(); 1055 | } 1056 | finally 1057 | { 1058 | module.Terminate(); 1059 | EditorUtility.ClearProgressBar(); 1060 | } 1061 | } 1062 | 1063 | private static void Colored(bool isActive, Color bgColor, Action inside) 1064 | { 1065 | var col = GUI.contentColor; 1066 | try 1067 | { 1068 | if (isActive) GUI.contentColor = bgColor; 1069 | inside(); 1070 | } 1071 | finally 1072 | { 1073 | GUI.contentColor = col; 1074 | } 1075 | } 1076 | 1077 | private static T ColoredReturning(bool isActive, Color bgColor, Func inside) 1078 | { 1079 | var col = GUI.contentColor; 1080 | try 1081 | { 1082 | if (isActive) GUI.contentColor = bgColor; 1083 | return inside(); 1084 | } 1085 | finally 1086 | { 1087 | GUI.contentColor = col; 1088 | } 1089 | } 1090 | 1091 | private static bool ColoredBgButton(bool isActive, Color bgColor, Func inside) 1092 | { 1093 | var col = GUI.backgroundColor; 1094 | try 1095 | { 1096 | if (isActive) GUI.backgroundColor = bgColor; 1097 | return inside(); 1098 | } 1099 | finally 1100 | { 1101 | GUI.backgroundColor = col; 1102 | } 1103 | } 1104 | 1105 | private static AnimationClip EmptyClip() 1106 | { 1107 | var emptyClip = new AnimationClip(); 1108 | AnimationUtility.SetEditorCurve( 1109 | emptyClip, 1110 | new EditorCurveBinding 1111 | { 1112 | path = "_ignored", 1113 | type = typeof(GameObject), 1114 | propertyName = "m_Active" 1115 | }, 1116 | AnimationCurve.Constant(0, 1 / 60f, 100f) 1117 | ); 1118 | return emptyClip; 1119 | } 1120 | 1121 | private Texture2D NewTexture() 1122 | { 1123 | var newTexture = new Texture2D(Mathf.Max(thumbnailSize, MinWidth), thumbnailSize, TextureFormat.RGB24, false); 1124 | newTexture.wrapMode = TextureWrapMode.Clamp; 1125 | return newTexture; 1126 | } 1127 | 1128 | [MenuItem("Window/Haï/VisualExpressionsEditor")] 1129 | public static void ShowWindow() 1130 | { 1131 | Obtain().Show(); 1132 | } 1133 | 1134 | [MenuItem("CONTEXT/Animator/Haï VisualExpressionsEditor")] 1135 | public static void OpenEditor(MenuCommand command) 1136 | { 1137 | var window = Obtain(); 1138 | window.UsingAnimator((Animator) command.context); 1139 | window.Show(); 1140 | window.TryExecuteAnimatorUpdate(); 1141 | } 1142 | 1143 | private static VisualExpressionsEditorWindow Obtain() 1144 | { 1145 | var editor = GetWindow(false, null, false); 1146 | editor.titleContent = new GUIContent("VisualExpressionsEditor"); 1147 | return editor; 1148 | } 1149 | } 1150 | } -------------------------------------------------------------------------------- /Assets/Hai/VisualExpressionsEditor/Scripts/Editor/VisualExpressionsEditorWindow.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 588354877735bc6498e892cf7e3e9202 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Assets/Hai/VisualExpressionsEditor/Shaders.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 3daabfb07834da742bd5bb666ea2dd82 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Assets/Hai/VisualExpressionsEditor/Shaders/HaiVisualExpressionsEditor.shader: -------------------------------------------------------------------------------- 1 | Shader "Hai/VisualExpressionsEditor" 2 | { 3 | Properties 4 | { 5 | _MainTex ("Morphed Texture", 2D) = "white" {} 6 | _NeutralTex ("Neutral Texture", 2D) = "white" {} 7 | _Hotspots ("Hotspots", Range(0, 1)) = 0 8 | } 9 | SubShader 10 | { 11 | Tags { "RenderType"="Opaque" } 12 | LOD 100 13 | 14 | Pass 15 | { 16 | CGPROGRAM 17 | #pragma vertex vert 18 | #pragma fragment frag 19 | 20 | #include "UnityCG.cginc" 21 | 22 | struct appdata 23 | { 24 | float4 vertex : POSITION; 25 | float2 uv : TEXCOORD0; 26 | }; 27 | 28 | struct v2f 29 | { 30 | float2 uv : TEXCOORD0; 31 | UNITY_FOG_COORDS(1) 32 | float4 vertex : SV_POSITION; 33 | float4 diff : TEXCOORD1; 34 | }; 35 | 36 | sampler2D _MainTex; 37 | float4 _MainTex_ST; 38 | float4 _MainTex_TexelSize; 39 | 40 | sampler2D _NeutralTex; 41 | float4 _NeutralTex_ST; 42 | 43 | float _Hotspots; 44 | 45 | v2f vert (appdata v) 46 | { 47 | v2f o; 48 | o.vertex = UnityObjectToClipPos(v.vertex); 49 | o.uv = TRANSFORM_TEX(v.uv, _MainTex); 50 | 51 | float width = _MainTex_TexelSize.z; 52 | float height = _MainTex_TexelSize.w; 53 | float4 difference = float4(width, height, 0, 0); 54 | // TODO: There's gotta be a more clever way to do this that uses parallelism 55 | for (int y = 0; y < height; y++) { 56 | for (int x = 0; x < width; x++) { 57 | float4 sampleLocation = float4(x / width, y / height, 0, 0); 58 | fixed3 neutral = tex2Dlod(_NeutralTex, sampleLocation).xyz; 59 | fixed3 morphed = tex2Dlod(_MainTex, sampleLocation).xyz; 60 | fixed3 v = neutral - morphed; 61 | if (dot(v, v) > 0.01) { 62 | difference = float4( 63 | min(x, difference.x), 64 | min(y, difference.y), 65 | max(x, difference.z), 66 | max(y, difference.w) 67 | ); 68 | } 69 | } 70 | } 71 | o.diff = difference; 72 | 73 | return o; 74 | } 75 | 76 | fixed4 frag (v2f i) : SV_Target 77 | { 78 | float width = _MainTex_TexelSize.z; 79 | float height = _MainTex_TexelSize.w; 80 | 81 | float4 difference = i.diff; 82 | difference = difference + float4(-1, -1, 1, 1) * 2; // Margin 83 | 84 | fixed4 col = tex2D(_MainTex, i.uv); 85 | if (i.uv.x < difference.x / width || i.uv.x > difference.z / width 86 | || i.uv.y < difference.y / height || i.uv.y > difference.w / height) { 87 | col.xyz = col.xyz * 0.2; 88 | } 89 | if (_Hotspots > 0.01) { 90 | fixed3 neutral = tex2D(_NeutralTex, i.uv).xyz; 91 | fixed3 morphed = tex2D(_MainTex, i.uv).xyz; 92 | col = lerp(col, length(neutral - morphed) * float4(1, 0, 0, 1), _Hotspots); 93 | } 94 | return col; 95 | } 96 | ENDCG 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Assets/Hai/VisualExpressionsEditor/Shaders/HaiVisualExpressionsEditor.shader.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: fbeba698875092e41a9e8e67ebb782db 3 | timeCreated: 1642187788 -------------------------------------------------------------------------------- /Assets/Hai/VisualExpressionsEditor/Shaders/HaiVisualExpressionsEditorRectOnly.shader: -------------------------------------------------------------------------------- 1 | Shader "Hai/VisualExpressionsEditorRectOnly" 2 | { 3 | Properties 4 | { 5 | _MainTex ("Morphed Texture", 2D) = "white" {} 6 | _NeutralTex ("Neutral Texture", 2D) = "white" {} 7 | _Hotspots ("Hotspots", Range(0, 1)) = 0 8 | _Rect ("Rect", Vector) = (0, 0, 0, 0) 9 | } 10 | SubShader 11 | { 12 | Tags { "RenderType"="Opaque" } 13 | LOD 100 14 | 15 | Pass 16 | { 17 | CGPROGRAM 18 | #pragma vertex vert 19 | #pragma fragment frag 20 | 21 | #include "UnityCG.cginc" 22 | 23 | struct appdata 24 | { 25 | float4 vertex : POSITION; 26 | float2 uv : TEXCOORD0; 27 | }; 28 | 29 | struct v2f 30 | { 31 | float2 uv : TEXCOORD0; 32 | UNITY_FOG_COORDS(1) 33 | float4 vertex : SV_POSITION; 34 | }; 35 | 36 | sampler2D _MainTex; 37 | float4 _MainTex_ST; 38 | float4 _MainTex_TexelSize; 39 | 40 | sampler2D _NeutralTex; 41 | float4 _NeutralTex_ST; 42 | 43 | float _Hotspots; 44 | float4 _Rect; 45 | 46 | v2f vert (appdata v) 47 | { 48 | v2f o; 49 | o.vertex = UnityObjectToClipPos(v.vertex); 50 | o.uv = TRANSFORM_TEX(v.uv, _MainTex); 51 | 52 | return o; 53 | } 54 | 55 | fixed4 frag (v2f i) : SV_Target 56 | { 57 | float width = _MainTex_TexelSize.z; 58 | float height = _MainTex_TexelSize.w; 59 | 60 | float4 difference = _Rect; 61 | difference = difference + float4(-1, -1, 1, 1) * 2; // Margin 62 | 63 | fixed4 col = tex2D(_MainTex, i.uv); 64 | if (i.uv.x < difference.x / width || i.uv.x > difference.z / width 65 | || i.uv.y < difference.y / height || i.uv.y > difference.w / height) { 66 | col.xyz = col.xyz * 0.2; 67 | } 68 | if (_Hotspots > 0.01) { 69 | fixed3 neutral = tex2D(_NeutralTex, i.uv).xyz; 70 | fixed3 morphed = tex2D(_MainTex, i.uv).xyz; 71 | col = lerp(col, length(neutral - morphed) * float4(1, 0, 0, 1), _Hotspots); 72 | } 73 | return col; 74 | } 75 | ENDCG 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Assets/Hai/VisualExpressionsEditor/Shaders/HaiVisualExpressionsEditorRectOnly.shader.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: d4a4348a5061fc24493d4fd175a5cf31 3 | timeCreated: 1642206672 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Haï~ (@vr_hai github.com/hai-vr) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | The projects of this repository were migrated into three different GitHub repos: 🟡 [Blendshape Viewer](https://github.com/hai-vr/blendshape-viewer-vcc), 🔵 [Animation Viewer](https://github.com/hai-vr/animation-viewer-vcc), and ⚪ [Visual Expressions Editor](https://github.com/hai-vr/visual-expressions-editor-vcc). 2 | 3 | # 🟡 Blendshape Viewer 4 | 5 | ### [> Download latest version of 🟡 Blendshape Viewer...](https://github.com/hai-vr/blendshape-viewer-vcc) 6 | 7 | *Blendshape Viewer* lets you visually browse blendshapes. 8 | 9 | https://user-images.githubusercontent.com/60819407/149598206-be8e4d7d-623c-4dea-8763-b482600c4c7c.mp4 10 | 11 | # 🔵 Animation Viewer 12 | 13 | ### [> Download latest version of 🔵 Animation Viewer...](https://github.com/hai-vr/animation-viewer-vcc) 14 | 15 | *Animation Viewer* lets you preview animations in the Project view browser. 16 | 17 | https://user-images.githubusercontent.com/60819407/152086223-b1eff9ca-d46f-4a2f-adc7-58263f45bc25.mp4 18 | 19 | # ⚪ Visual Expressions Editor 20 | 21 | ### [> Download latest version of ⚪ Visual Expressions Editor...](https://github.com/hai-vr/visual-expressions-editor-vcc) 22 | 23 | *Visual Expressions Editor* lets you edit face expression animations. 24 | 25 | https://user-images.githubusercontent.com/60819407/157273809-6418d258-bb78-4add-9ce8-ac9659937d91.mp4 26 | --------------------------------------------------------------------------------