From 8418f8e047730ca223e6288acb86de86894640a1 Mon Sep 17 00:00:00 2001 From: nekobako Date: Thu, 22 Aug 2024 12:27:10 +0900 Subject: [PATCH] feat: change shape changer to support multiple target renderers (#1011) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add target renderer to ChangedShape * chore: add test for ShapeChanger target renderer * feat: add override target to MaterialSetter * fix: resolve added AvatarObjectReference * fix: record prefab instance property modifications * refactor: remove unused setter for AvatarObjectReference * refactor: change ChangedShape and MaterialSwitchObject from struct to class * feat: remove override target from ShapeChanger and MaterialSetter * refactor: align flow and code style of ShapeChanger and MaterialSetter * feat: ShapeChanger target migration * fix: add null check * chore: added some comments and nullchecks --------- Co-authored-by: bd_ --- .../MaterialSwitchObjectEditor.cs | 55 +--- .../MaterialSwitchObjectEditor.uxml | 6 +- .../Inspector/ShapeChanger/AddShapePopup.cs | 99 ------- .../ShapeChanger/AddShapePopup.cs.meta | 3 - .../Inspector/ShapeChanger/AddShapePopup.uxml | 11 - .../ShapeChanger/AddShapePopup.uxml.meta | 3 - .../ShapeChanger/ChangedShapeEditor.cs | 36 +++ .../ShapeChanger/ChangedShapeEditor.uxml | 13 +- .../Inspector/ShapeChanger/ShapeChanger.uxml | 1 - .../ShapeChanger/ShapeChangerEditor.cs | 48 +++- .../ShapeChanger/ShapeChangerStyles.uss | 11 +- Editor/Localization/en-US.json | 1 - Editor/Localization/ja-JP.json | 1 - Editor/Localization/zh-Hant.json | 1 - .../ReactiveObjectAnalyzer.LocateReactions.cs | 17 +- .../ReactiveObjects/MaterialSetterPreview.cs | 92 +++--- Editor/ReactiveObjects/ShapeChangerPreview.cs | 263 +++++++++--------- .../ModularAvatarMaterialSetter.cs | 2 +- .../ModularAvatarShapeChanger.cs | 60 +++- 19 files changed, 334 insertions(+), 389 deletions(-) delete mode 100644 Editor/Inspector/ShapeChanger/AddShapePopup.cs delete mode 100644 Editor/Inspector/ShapeChanger/AddShapePopup.cs.meta delete mode 100644 Editor/Inspector/ShapeChanger/AddShapePopup.uxml delete mode 100644 Editor/Inspector/ShapeChanger/AddShapePopup.uxml.meta diff --git a/Editor/Inspector/MaterialSetter/MaterialSwitchObjectEditor.cs b/Editor/Inspector/MaterialSetter/MaterialSwitchObjectEditor.cs index ca9697c0..398b9ea7 100644 --- a/Editor/Inspector/MaterialSetter/MaterialSwitchObjectEditor.cs +++ b/Editor/Inspector/MaterialSetter/MaterialSwitchObjectEditor.cs @@ -28,44 +28,14 @@ namespace nadena.dev.modular_avatar.core.editor.ShapeChanger var f_material_index = uxml.Q("f-material-index"); - var f_object = uxml.Q("f-object"); - f_object.objectType = typeof(Renderer); - f_object.allowSceneObjects = true; + var f_object = uxml.Q("f-object"); - var f_target_object = uxml.Q("f-obj-target-object"); - var f_reference_path = uxml.Q("f-obj-ref-path"); - - f_object.RegisterValueChangedCallback(evt => + f_object.RegisterValueChangeCallback(evt => { - var gameObj = (evt.newValue as Renderer)?.gameObject; - - if (gameObj == null) - { - f_target_object.value = null; - f_reference_path.value = ""; - } - else - { - var path = RuntimeUtil.AvatarRootPath(gameObj); - - f_reference_path.value = path; - if (path == "") - { - f_target_object.value = null; - } - else - { - f_target_object.value = gameObj; - } - } - EditorApplication.delayCall += UpdateMaterialDropdown; }); UpdateMaterialDropdown(); - f_target_object.RegisterValueChangedCallback(_ => UpdateVisualTarget()); - f_reference_path.RegisterValueChangedCallback(_ => UpdateVisualTarget()); - // Link dropdown to material index field var f_material_index_int = uxml.Q("f-material-index-int"); f_material_index_int.RegisterValueChangedCallback(evt => @@ -83,30 +53,13 @@ namespace nadena.dev.modular_avatar.core.editor.ShapeChanger return uxml; - void UpdateVisualTarget() - { - var targetObject = AvatarObjectReference.Get(property.FindPropertyRelative("Object")); - Renderer targetRenderer; - - try - { - targetRenderer = targetObject?.GetComponent(); - } - catch (MissingComponentException e) - { - targetRenderer = null; - } - - f_object.SetValueWithoutNotify(targetRenderer); - } - void UpdateMaterialDropdown() { - var toggledObject = AvatarObjectReference.Get(property.FindPropertyRelative("Object")); + var targetObject = AvatarObjectReference.Get(property.FindPropertyRelative("Object")); Material[] sharedMaterials; try { - sharedMaterials = toggledObject?.GetComponent()?.sharedMaterials; + sharedMaterials = targetObject?.GetComponent()?.sharedMaterials; } catch (MissingComponentException e) { diff --git a/Editor/Inspector/MaterialSetter/MaterialSwitchObjectEditor.uxml b/Editor/Inspector/MaterialSetter/MaterialSwitchObjectEditor.uxml index ca9afd40..2be7181c 100644 --- a/Editor/Inspector/MaterialSetter/MaterialSwitchObjectEditor.uxml +++ b/Editor/Inspector/MaterialSetter/MaterialSwitchObjectEditor.uxml @@ -1,13 +1,9 @@  - - + - - - diff --git a/Editor/Inspector/ShapeChanger/AddShapePopup.cs b/Editor/Inspector/ShapeChanger/AddShapePopup.cs deleted file mode 100644 index ec0e32c5..00000000 --- a/Editor/Inspector/ShapeChanger/AddShapePopup.cs +++ /dev/null @@ -1,99 +0,0 @@ -#region - -using System.Collections.Generic; -using System.Linq; -using UnityEditor; -using UnityEngine; -using UnityEngine.UIElements; - -#endregion - -namespace nadena.dev.modular_avatar.core.editor.ShapeChanger -{ - public class AddShapePopup : PopupWindowContent - { - private const string Root = "Packages/nadena.dev.modular-avatar/Editor/Inspector/ShapeChanger/"; - const string UxmlPath = Root + "AddShapePopup.uxml"; - const string UssPath = Root + "ShapeChangerStyles.uss"; - - private VisualElement _elem; - private ScrollView _scrollView; - - public AddShapePopup(ModularAvatarShapeChanger changer) - { - if (changer == null) return; - var target = changer.targetRenderer.Get(changer)?.GetComponent(); - if (target == null || target.sharedMesh == null) return; - - var alreadyRegistered = changer.Shapes.Select(c => c.ShapeName).ToHashSet(); - - var keys = new List(); - for (int i = 0; i < target.sharedMesh.blendShapeCount; i++) - { - var name = target.sharedMesh.GetBlendShapeName(i); - if (alreadyRegistered.Contains(name)) continue; - - keys.Add(name); - } - - var uss = AssetDatabase.LoadAssetAtPath(UssPath); - var uxml = AssetDatabase.LoadAssetAtPath(UxmlPath); - - _elem = uxml.CloneTree(); - _elem.styleSheets.Add(uss); - Localization.UI.Localize(_elem); - - _scrollView = _elem.Q("scroll-view"); - - if (keys.Count > 0) - { - _scrollView.contentContainer.Clear(); - - foreach (var key in keys) - { - var container = new VisualElement(); - container.AddToClassList("add-shape-row"); - - Button btn = default; - btn = new Button(() => - { - AddShape(changer, key); - container.RemoveFromHierarchy(); - }); - btn.text = "+"; - container.Add(btn); - - var label = new Label(key); - container.Add(label); - - _scrollView.contentContainer.Add(container); - } - } - } - - private void AddShape(ModularAvatarShapeChanger changer, string key) - { - Undo.RecordObject(changer, "Add Shape"); - - changer.Shapes.Add(new ChangedShape() - { - ShapeName = key, - ChangeType = ShapeChangeType.Delete, - Value = 100 - }); - } - - public override void OnGUI(Rect rect) - { - } - - public override void OnOpen() - { - editorWindow.rootVisualElement.Clear(); - editorWindow.rootVisualElement.Add(_elem); - //editorWindow.rootVisualElement.Clear(); - - //editorWindow.rootVisualElement.Add(new Label("Hello, World!")); - } - } -} \ No newline at end of file diff --git a/Editor/Inspector/ShapeChanger/AddShapePopup.cs.meta b/Editor/Inspector/ShapeChanger/AddShapePopup.cs.meta deleted file mode 100644 index daad933e..00000000 --- a/Editor/Inspector/ShapeChanger/AddShapePopup.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 1a8351fafb3740918363f60365adfeda -timeCreated: 1717205112 \ No newline at end of file diff --git a/Editor/Inspector/ShapeChanger/AddShapePopup.uxml b/Editor/Inspector/ShapeChanger/AddShapePopup.uxml deleted file mode 100644 index 85ec5d1e..00000000 --- a/Editor/Inspector/ShapeChanger/AddShapePopup.uxml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/Editor/Inspector/ShapeChanger/AddShapePopup.uxml.meta b/Editor/Inspector/ShapeChanger/AddShapePopup.uxml.meta deleted file mode 100644 index bbd7c309..00000000 --- a/Editor/Inspector/ShapeChanger/AddShapePopup.uxml.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 6753a7b3eae1416cb04786cf53778c33 -timeCreated: 1717205258 \ No newline at end of file diff --git a/Editor/Inspector/ShapeChanger/ChangedShapeEditor.cs b/Editor/Inspector/ShapeChanger/ChangedShapeEditor.cs index d8588535..5c4a31f5 100644 --- a/Editor/Inspector/ShapeChanger/ChangedShapeEditor.cs +++ b/Editor/Inspector/ShapeChanger/ChangedShapeEditor.cs @@ -1,7 +1,10 @@ #region +using System.Collections.Generic; +using System.Linq; using UnityEditor; using UnityEditor.UIElements; +using UnityEngine; using UnityEngine.UIElements; #endregion @@ -24,6 +27,16 @@ namespace nadena.dev.modular_avatar.core.editor.ShapeChanger uxml.styleSheets.Add(uss); uxml.BindProperty(property); + var f_shape_name = uxml.Q("f-shape-name"); + + var f_object = uxml.Q("f-object"); + + f_object.RegisterValueChangeCallback(evt => + { + EditorApplication.delayCall += UpdateShapeDropdown; + }); + UpdateShapeDropdown(); + uxml.Q("f-change-type").RegisterCallback>( e => { @@ -39,6 +52,29 @@ namespace nadena.dev.modular_avatar.core.editor.ShapeChanger ); return uxml; + + void UpdateShapeDropdown() + { + var targetObject = AvatarObjectReference.Get(property.FindPropertyRelative("Object")); + List shapeNames; + try + { + var mesh = targetObject?.GetComponent()?.sharedMesh; + shapeNames = mesh == null ? null : Enumerable.Range(0, mesh.blendShapeCount) + .Select(x => mesh.GetBlendShapeName(x)) + .ToList(); + } + catch (MissingComponentException e) + { + shapeNames = null; + } + + f_shape_name.SetEnabled(shapeNames != null); + f_shape_name.choices = shapeNames ?? new(); + + f_shape_name.formatListItemCallback = name => shapeNames != null ? name : ""; + f_shape_name.formatSelectedValueCallback = f_shape_name.formatListItemCallback; + } } } } \ No newline at end of file diff --git a/Editor/Inspector/ShapeChanger/ChangedShapeEditor.uxml b/Editor/Inspector/ShapeChanger/ChangedShapeEditor.uxml index e2cf8875..d5ac9260 100644 --- a/Editor/Inspector/ShapeChanger/ChangedShapeEditor.uxml +++ b/Editor/Inspector/ShapeChanger/ChangedShapeEditor.uxml @@ -1,8 +1,13 @@  - - - - + + + + + + + + + \ No newline at end of file diff --git a/Editor/Inspector/ShapeChanger/ShapeChanger.uxml b/Editor/Inspector/ShapeChanger/ShapeChanger.uxml index b3b5db99..90e0c8e2 100644 --- a/Editor/Inspector/ShapeChanger/ShapeChanger.uxml +++ b/Editor/Inspector/ShapeChanger/ShapeChanger.uxml @@ -2,7 +2,6 @@ xmlns:ma="nadena.dev.modular_avatar.core.editor"> - diff --git a/Editor/Inspector/ShapeChanger/ShapeChangerEditor.cs b/Editor/Inspector/ShapeChanger/ShapeChangerEditor.cs index 8ade9ee4..ea0fcdf7 100644 --- a/Editor/Inspector/ShapeChanger/ShapeChangerEditor.cs +++ b/Editor/Inspector/ShapeChanger/ShapeChangerEditor.cs @@ -1,6 +1,7 @@ #region using System; +using System.Linq; using UnityEditor; using UnityEditor.UIElements; using UnityEngine; @@ -18,6 +19,7 @@ namespace nadena.dev.modular_avatar.core.editor.ShapeChanger [SerializeField] private StyleSheet uss; [SerializeField] private VisualTreeAsset uxml; + private BlendshapeSelectWindow _window; protected override void OnInnerInspectorGUI() { @@ -41,12 +43,50 @@ namespace nadena.dev.modular_avatar.core.editor.ShapeChanger var field_addButton = typeof(BaseListView).GetField("m_AddButton", NonPublic | Instance); var addButton = (Button)field_addButton.GetValue(listView); - addButton.clickable = new Clickable(() => - { - PopupWindow.Show(addButton.worldBound, new AddShapePopup(target as ModularAvatarShapeChanger)); - }); + addButton.clickable = new Clickable(OpenAddWindow); return root; } + + private void OnDisable() + { + if (_window != null) DestroyImmediate(_window); + } + + protected override void OnDestroy() + { + base.OnDestroy(); + if (_window != null) DestroyImmediate(_window); + } + + private void OpenAddWindow() + { + if (_window != null) DestroyImmediate(_window); + _window = CreateInstance(); + _window.AvatarRoot = RuntimeUtil.FindAvatarTransformInParents((target as ModularAvatarShapeChanger).transform).gameObject; + _window.OfferBinding += OfferBinding; + _window.Show(); + } + + private void OfferBinding(BlendshapeBinding binding) + { + var changer = target as ModularAvatarShapeChanger; + if (changer.Shapes.Any(x => x.Object.Equals(binding.ReferenceMesh) && x.ShapeName == binding.Blendshape)) + { + return; + } + + Undo.RecordObject(changer, "Add Shape"); + + changer.Shapes.Add(new ChangedShape() + { + Object = binding.ReferenceMesh, + ShapeName = binding.Blendshape, + ChangeType = ShapeChangeType.Delete, + Value = 100 + }); + + PrefabUtility.RecordPrefabInstancePropertyModifications(changer); + } } } \ No newline at end of file diff --git a/Editor/Inspector/ShapeChanger/ShapeChangerStyles.uss b/Editor/Inspector/ShapeChanger/ShapeChangerStyles.uss index ebd9a7ef..f06b0cc7 100644 --- a/Editor/Inspector/ShapeChanger/ShapeChangerStyles.uss +++ b/Editor/Inspector/ShapeChanger/ShapeChangerStyles.uss @@ -39,11 +39,15 @@ flex-direction: row; } -.changed-shape-editor { +.changed-shape-editor .horizontal { flex-direction: row; } -.changed-shape-editor #f-name { +.changed-shape-editor #f-object { + flex-grow: 1; +} + +.changed-shape-editor #f-shape-name { flex-grow: 1; } @@ -64,7 +68,7 @@ } .f-value { - width: 40px; + width: 75px; } #f-value-delete { @@ -77,6 +81,7 @@ .change-type-delete #f-value-delete { display: flex; + height: 20px; } /* Add shape window */ diff --git a/Editor/Localization/en-US.json b/Editor/Localization/en-US.json index de228ac4..4040d9a2 100644 --- a/Editor/Localization/en-US.json +++ b/Editor/Localization/en-US.json @@ -251,6 +251,5 @@ "ma_info.param_usage_ui.no_data": "[ NO DATA ]", "reactive_object.inverse": "Inverse Condition", "reactive_object.material-setter.set-to": "Set material to: ", - "reactive_object.shape-changer.target-renderer": "Target Renderer", "menuitem.misc.add_toggle": "Add toggle" } \ No newline at end of file diff --git a/Editor/Localization/ja-JP.json b/Editor/Localization/ja-JP.json index 83be9283..e56d18f5 100644 --- a/Editor/Localization/ja-JP.json +++ b/Editor/Localization/ja-JP.json @@ -247,6 +247,5 @@ "ma_info.param_usage_ui.no_data": "[ NO DATA ]", "reactive_object.inverse": "条件を反転", "reactive_object.material-setter.set-to": "変更先のマテリアル ", - "reactive_object.shape-changer.target-renderer": "操作するレンダラー", "menuitem.misc.add_toggle": "トグルを追加" } \ No newline at end of file diff --git a/Editor/Localization/zh-Hant.json b/Editor/Localization/zh-Hant.json index 65ece102..a956027b 100644 --- a/Editor/Localization/zh-Hant.json +++ b/Editor/Localization/zh-Hant.json @@ -250,6 +250,5 @@ "ma_info.param_usage_ui.no_data": "【無資訊】", "reactive_object.inverse": "反轉條件", "reactive_object.material-setter.set-to": "將材質設定為:", - "reactive_object.shape-changer.target-renderer": "目標 Renderer", "menuitem.misc.add_toggle": "新增開關" } \ No newline at end of file diff --git a/Editor/ReactiveObjects/AnimationGeneration/ReactiveObjectAnalyzer.LocateReactions.cs b/Editor/ReactiveObjects/AnimationGeneration/ReactiveObjectAnalyzer.LocateReactions.cs index 5ebdf2cf..8f6cada2 100644 --- a/Editor/ReactiveObjects/AnimationGeneration/ReactiveObjectAnalyzer.LocateReactions.cs +++ b/Editor/ReactiveObjects/AnimationGeneration/ReactiveObjectAnalyzer.LocateReactions.cs @@ -15,15 +15,16 @@ namespace nadena.dev.modular_avatar.core.editor foreach (var changer in changers) { - var renderer = changer.targetRenderer.Get(changer)?.GetComponent(); - if (renderer == null) continue; - - var mesh = renderer.sharedMesh; - - if (mesh == null) continue; + if (changer.Shapes == null) continue; foreach (var shape in changer.Shapes) { + var renderer = shape.Object.Get(changer)?.GetComponent(); + if (renderer == null) continue; + + var mesh = renderer.sharedMesh; + if (mesh == null) continue; + var shapeId = mesh.GetBlendShapeIndex(shape.ShapeName); if (shapeId < 0) continue; @@ -92,9 +93,7 @@ namespace nadena.dev.modular_avatar.core.editor foreach (var obj in setter.Objects) { - var target = obj.Object.Get(setter); - if (target == null) continue; - var renderer = target.GetComponent(); + var renderer = obj.Object.Get(setter)?.GetComponent(); if (renderer == null || renderer.sharedMaterials.Length < obj.MaterialIndex) continue; var key = new TargetProp diff --git a/Editor/ReactiveObjects/MaterialSetterPreview.cs b/Editor/ReactiveObjects/MaterialSetterPreview.cs index 4b8b6883..8ff7b2c8 100644 --- a/Editor/ReactiveObjects/MaterialSetterPreview.cs +++ b/Editor/ReactiveObjects/MaterialSetterPreview.cs @@ -31,40 +31,41 @@ namespace nadena.dev.modular_avatar.core.editor var menuItemPreview = new MenuItemPreviewCondition(context); var setters = context.GetComponentsByType(); - var groups = new Dictionary>(); + var builders = + new Dictionary.Builder>( + new ObjectIdentityComparer()); foreach (var setter in setters) { + if (setter == null) continue; + var mami = context.GetComponent(setter.gameObject); bool active = context.ActiveAndEnabled(setter) && (mami == null || menuItemPreview.IsEnabledForPreview(mami)); - if (active == context.Observe(setter, t => t.Inverted)) continue; + if (active == context.Observe(setter, s => s.Inverted)) continue; - var objs = context.Observe(setter, s => s.Objects.Select(o => (o.Object.Get(s), o.Material, o.MaterialIndex)).ToList(), (x, y) => x.SequenceEqual(y)); + var objects = context.Observe(setter, s => s.Objects.Select(o => (o.Object.Get(s), o.Material, o.MaterialIndex)).ToList(), Enumerable.SequenceEqual); - if (setter.Objects == null) continue; - - foreach (var (obj, mat, index) in objs) + foreach (var (target, mat, index) in objects) { - if (obj == null) continue; - var renderer = context.GetComponent(obj); + var renderer = context.GetComponent(target); if (renderer == null) continue; var matCount = context.Observe(renderer, r => r.sharedMaterials.Length); if (matCount <= index) continue; - if (!groups.TryGetValue(renderer, out var list)) + if (!builders.TryGetValue(renderer, out var builder)) { - list = ImmutableList.Create(); - groups.Add(renderer, list); + builder = ImmutableList.CreateBuilder(); + builders[renderer] = builder; } - groups[renderer] = list.Add(setter); + builder.Add(setter); } } - var finalGroups = groups.Select(g => RenderGroup.For(g.Key).WithData(g.Value)).ToImmutableList(); - return finalGroups; + return builders.Select(g => RenderGroup.For(g.Key).WithData(g.Value.ToImmutable())) + .ToImmutableList(); } public Task Instantiate(RenderGroup group, IEnumerable<(Renderer, Renderer)> proxyPairs, ComputeContext context) @@ -80,48 +81,33 @@ namespace nadena.dev.modular_avatar.core.editor private readonly ImmutableList _setters; private ImmutableList<(int, Material)> _materials; - public RenderAspects WhatChanged {get; private set; } - - public void OnFrame(Renderer original, Renderer proxy) - { - var mats = proxy.sharedMaterials; - - foreach (var mat in _materials) - { - if (mat.Item1 <= mats.Length) - { - mats[mat.Item1] = mat.Item2; - } - } - - proxy.sharedMaterials = mats; - } + public RenderAspects WhatChanged => RenderAspects.Material; public Node(ImmutableList setters) { _setters = setters; _materials = ImmutableList<(int, Material)>.Empty; - WhatChanged = RenderAspects.Material; } public Task Refresh(IEnumerable<(Renderer, Renderer)> proxyPairs, ComputeContext context, RenderAspects updatedAspects) { - var proxyPair = proxyPairs.First(); - var original = proxyPair.Item1; - var proxy = proxyPair.Item2; + var (original, proxy) = proxyPairs.First(); + + if (original == null || proxy == null) return null; var mats = new Material[proxy.sharedMaterials.Length]; foreach (var setter in _setters) { - var objects = context.Observe(setter, s => s.Objects - .Where(obj => obj.Object.Get(s) == original.gameObject) - .Select(obj => (obj.Material, obj.MaterialIndex)), - (x, y) => x.SequenceEqual(y) - ); + if (setter == null) continue; + + var objects = context.Observe(setter, s => s.Objects.Select(o => (o.Object.Get(s), o.Material, o.MaterialIndex)).ToList(), Enumerable.SequenceEqual); - foreach (var (mat, index) in objects) + foreach (var (target, mat, index) in objects) { + var renderer = context.GetComponent(target); + if (renderer != original) continue; + if (index <= mats.Length) { mats[index] = mat; @@ -131,14 +117,32 @@ namespace nadena.dev.modular_avatar.core.editor var materials = mats.Select((m, i) => (i, m)).Where(kvp => kvp.m != null).ToImmutableList(); - if (materials.SequenceEqual(_materials)) + if (!materials.SequenceEqual(_materials)) { - return Task.FromResult(this); + return Task.FromResult(new Node(_setters) + { + _materials = materials, + }); } - else + + return Task.FromResult(this); + } + + public void OnFrame(Renderer original, Renderer proxy) + { + if (original == null || proxy == null) return; + + var mats = proxy.sharedMaterials; + + foreach (var mat in _materials) { - return Task.FromResult(new Node(_setters) { _materials = materials }); + if (mat.Item1 <= mats.Length) + { + mats[mat.Item1] = mat.Item2; + } } + + proxy.sharedMaterials = mats; } } } diff --git a/Editor/ReactiveObjects/ShapeChangerPreview.cs b/Editor/ReactiveObjects/ShapeChangerPreview.cs index d5aa2efc..ba862cf4 100644 --- a/Editor/ReactiveObjects/ShapeChangerPreview.cs +++ b/Editor/ReactiveObjects/ShapeChangerPreview.cs @@ -31,79 +31,107 @@ namespace nadena.dev.modular_avatar.core.editor return context.Observe(EnableNode.IsEnabled); } - public ImmutableList GetTargetGroups(ComputeContext ctx) + public ImmutableList GetTargetGroups(ComputeContext context) { - var menuItemPreview = new MenuItemPreviewCondition(ctx); - - var allChangers = ctx.GetComponentsByType(); + var menuItemPreview = new MenuItemPreviewCondition(context); + var changers = context.GetComponentsByType(); - var groups = + var builders = new Dictionary.Builder>( new ObjectIdentityComparer()); - foreach (var changer in allChangers) + foreach (var changer in changers) { if (changer == null) continue; - var mami = ctx.GetComponent(changer.gameObject); - bool active = ctx.ActiveAndEnabled(changer) && (mami == null || menuItemPreview.IsEnabledForPreview(mami)); - if (active == ctx.Observe(changer, t => t.Inverted)) continue; + var mami = context.GetComponent(changer.gameObject); + bool active = context.ActiveAndEnabled(changer) && (mami == null || menuItemPreview.IsEnabledForPreview(mami)); + if (active == context.Observe(changer, c => c.Inverted)) continue; - var target = ctx.Observe(changer, _ => changer.targetRenderer.Get(changer)); - var renderer = ctx.GetComponent(target); + var shapes = context.Observe(changer, c => c.Shapes.Select(s => (s.Object.Get(c), s.ShapeName, s.ChangeType, s.Value)).ToList(), Enumerable.SequenceEqual); - if (renderer == null) continue; - - if (!groups.TryGetValue(renderer, out var group)) + foreach (var (target, name, type, value) in shapes) { - group = ImmutableList.CreateBuilder(); - groups[renderer] = group; - } + var renderer = context.GetComponent(target); + if (renderer == null) continue; - group.Add(changer); + if (!builders.TryGetValue(renderer, out var builder)) + { + builder = ImmutableList.CreateBuilder(); + builders[renderer] = builder; + } + + builder.Add(changer); + } } - return groups.Select(g => RenderGroup.For(g.Key).WithData(g.Value.ToImmutable())) + return builders.Select(g => RenderGroup.For(g.Key).WithData(g.Value.ToImmutable())) .ToImmutableList(); } - public async Task Instantiate( - RenderGroup group, - IEnumerable<(Renderer, Renderer)> proxyPairs, - ComputeContext context) + public Task Instantiate(RenderGroup group, IEnumerable<(Renderer, Renderer)> proxyPairs, ComputeContext context) { - var node = new Node(group); + var changers = group.GetData>(); + var node = new Node(changers); - try - { - await node.Init(proxyPairs, context); - } - catch (Exception e) - { - // dispose - throw; - } - - return node; + return node.Refresh(proxyPairs, context, 0); } private class Node : IRenderFilterNode { - private readonly RenderGroup _group; private readonly ImmutableList _changers; - + private ImmutableHashSet<(int, float)> _shapes; + private ImmutableHashSet _toDelete; private Mesh _generatedMesh = null; - private HashSet _toDelete; - internal Node(RenderGroup group) + public RenderAspects WhatChanged => RenderAspects.Shapes | RenderAspects.Mesh; + + internal Node(ImmutableList changers) { - _group = group; - _changers = _group.GetData>(); + _changers = changers; + _shapes = ImmutableHashSet<(int, float)>.Empty; + _toDelete = ImmutableHashSet.Empty; + _generatedMesh = null; } - private HashSet GetToDeleteSet(SkinnedMeshRenderer proxy, ComputeContext context) + public Task Refresh(IEnumerable<(Renderer, Renderer)> proxyPairs, ComputeContext context, RenderAspects updatedAspects) { - var toDelete = new HashSet(); + var (original, proxy) = proxyPairs.First(); + + if (original == null || proxy == null) return null; + if (original is not SkinnedMeshRenderer originalSmr || proxy is not SkinnedMeshRenderer proxySmr) return null; + + var shapes = GetShapesSet(originalSmr, proxySmr, context); + var toDelete = GetToDeleteSet(originalSmr, proxySmr, context); + + if (!toDelete.SequenceEqual(_toDelete)) + { + return Task.FromResult(new Node(_changers) + { + _shapes = shapes, + _toDelete = toDelete, + _generatedMesh = GetGeneratedMesh(proxySmr, toDelete), + }); + } + + if (!shapes.SequenceEqual(_shapes)) + { + var reusableMesh = _generatedMesh; + _generatedMesh = null; + return Task.FromResult(new Node(_changers) + { + _shapes = shapes, + _toDelete = toDelete, + _generatedMesh = reusableMesh, + }); + } + + return Task.FromResult(this); + } + + private ImmutableHashSet<(int, float)> GetShapesSet(SkinnedMeshRenderer original, SkinnedMeshRenderer proxy, ComputeContext context) + { + var builder = ImmutableHashSet.CreateBuilder<(int, float)>(); var mesh = context.Observe(proxy, p => p.sharedMesh, (a, b) => { if (a != b) @@ -117,67 +145,60 @@ namespace nadena.dev.modular_avatar.core.editor foreach (var changer in _changers) { - var shapes = context.Observe(changer, - c => c.Shapes - .Where(s => s.ChangeType == ShapeChangeType.Delete) - .Select(s => s.ShapeName) - .ToImmutableList(), - Enumerable.SequenceEqual - ); + if (changer == null) continue; - foreach (var shape in shapes) + var shapes = context.Observe(changer, c => c.Shapes.Select(s => (s.Object.Get(c), s.ShapeName, s.ChangeType, s.Value)).ToList(), Enumerable.SequenceEqual); + + foreach (var (target, name, type, value) in shapes) { - var index = mesh.GetBlendShapeIndex(shape); + var renderer = context.GetComponent(target); + if (renderer != original) continue; + + var index = mesh.GetBlendShapeIndex(name); if (index < 0) continue; - toDelete.Add(index); + builder.Add((index, type == ShapeChangeType.Delete ? 100 : value)); } } - return toDelete; + return builder.ToImmutable(); } - public async Task Init( - IEnumerable<(Renderer, Renderer)> renderers, - ComputeContext context - ) + private ImmutableHashSet GetToDeleteSet(SkinnedMeshRenderer original, SkinnedMeshRenderer proxy, ComputeContext context) { - var (original, proxy) = renderers.First(); - - if (original == null || proxy == null) return; - if (!(proxy is SkinnedMeshRenderer smr)) return; - - await Init(smr, context, GetToDeleteSet(smr, context)); - } - - public async Task Refresh(IEnumerable<(Renderer, Renderer)> proxyPairs, - ComputeContext context, RenderAspects updatedAspects) - { - if ((updatedAspects & RenderAspects.Mesh) != 0) return null; - - var (original, proxy) = proxyPairs.First(); - - if (original == null || proxy == null) return null; - if (!(proxy is SkinnedMeshRenderer smr)) return null; - - var toDelete = GetToDeleteSet(smr, context); - if (toDelete.Count == _toDelete.Count && toDelete.All(_toDelete.Contains)) + var builder = ImmutableHashSet.CreateBuilder(); + var mesh = context.Observe(proxy, p => p.sharedMesh, (a, b) => { - //System.Diagnostics.Debug.WriteLine("[ShapeChangerPreview] No changes detected. Retaining node."); - return this; + if (a != b) + { + Debug.Log($"mesh changed {a.GetInstanceID()} -> {b.GetInstanceID()}"); + return false; + } + + return true; + }); + + foreach (var changer in _changers) + { + var shapes = context.Observe(changer, c => c.Shapes.Select(s => (s.Object.Get(c), s.ShapeName, s.ChangeType, s.Value)).ToList(), Enumerable.SequenceEqual); + + foreach (var (target, name, type, value) in shapes) + { + if (type != ShapeChangeType.Delete) continue; + + var renderer = context.GetComponent(target); + if (renderer != original) continue; + + var index = mesh.GetBlendShapeIndex(name); + if (index < 0) continue; + builder.Add(index); + } } - var node = new Node(_group); - await node.Init(smr, context, toDelete); - return node; + return builder.ToImmutable(); } - public async Task Init( - SkinnedMeshRenderer proxy, - ComputeContext context, - HashSet toDelete - ) + public Mesh GetGeneratedMesh(SkinnedMeshRenderer proxy, ImmutableHashSet toDelete) { - _toDelete = toDelete; var mesh = proxy.sharedMesh; if (toDelete.Count > 0) @@ -223,61 +244,33 @@ namespace nadena.dev.modular_avatar.core.editor mesh.SetTriangles(tris, subMesh, false, baseVertex: baseVertex); } - - _generatedMesh = mesh; + + return mesh; } + + return null; } + public void OnFrame(Renderer original, Renderer proxy) + { + if (original == null || proxy == null) return; + if (original is not SkinnedMeshRenderer originalSmr || proxy is not SkinnedMeshRenderer proxySmr) return; - public RenderAspects Reads => RenderAspects.Shapes | RenderAspects.Mesh; - public RenderAspects WhatChanged => RenderAspects.Shapes | RenderAspects.Mesh; + if (_generatedMesh != null) + { + proxySmr.sharedMesh = _generatedMesh; + } + + foreach (var shape in _shapes) + { + proxySmr.SetBlendShapeWeight(shape.Item1, shape.Item2); + } + } public void Dispose() { if (_generatedMesh != null) Object.DestroyImmediate(_generatedMesh); } - - public void OnFrame(Renderer original, Renderer proxy) - { - if (_changers == null) return; // can happen transiently as we disable the last component - if (!(proxy is SkinnedMeshRenderer smr)) return; - - Mesh mesh; - if (_generatedMesh != null) - { - smr.sharedMesh = _generatedMesh; - mesh = _generatedMesh; - } - else - { - mesh = smr.sharedMesh; - } - - if (mesh == null) return; - - foreach (var changer in _changers) - { - foreach (var shape in changer.Shapes) - { - var index = mesh.GetBlendShapeIndex(shape.ShapeName); - if (index < 0) continue; - - float setToValue = -1; - - switch (shape.ChangeType) - { - case ShapeChangeType.Delete: - setToValue = 100; - break; - case ShapeChangeType.Set: - setToValue = shape.Value; - break; - } - - smr.SetBlendShapeWeight(index, setToValue); - } - } - } } } } \ No newline at end of file diff --git a/Runtime/ReactiveObjects/ModularAvatarMaterialSetter.cs b/Runtime/ReactiveObjects/ModularAvatarMaterialSetter.cs index e9a50062..a6b949c2 100644 --- a/Runtime/ReactiveObjects/ModularAvatarMaterialSetter.cs +++ b/Runtime/ReactiveObjects/ModularAvatarMaterialSetter.cs @@ -5,7 +5,7 @@ using UnityEngine; namespace nadena.dev.modular_avatar.core { [Serializable] - public struct MaterialSwitchObject + public class MaterialSwitchObject { public AvatarObjectReference Object; public Material Material; diff --git a/Runtime/ReactiveObjects/ModularAvatarShapeChanger.cs b/Runtime/ReactiveObjects/ModularAvatarShapeChanger.cs index 1acc1157..e2be8753 100644 --- a/Runtime/ReactiveObjects/ModularAvatarShapeChanger.cs +++ b/Runtime/ReactiveObjects/ModularAvatarShapeChanger.cs @@ -17,15 +17,16 @@ namespace nadena.dev.modular_avatar.core } [Serializable] - public struct ChangedShape + public class ChangedShape { + public AvatarObjectReference Object; public string ShapeName; public ShapeChangeType ChangeType; public float Value; public bool Equals(ChangedShape other) { - return ShapeName == other.ShapeName && ChangeType == other.ChangeType && Value.Equals(other.Value); + return Equals(Object, other.Object) && ShapeName == other.ShapeName && ChangeType == other.ChangeType && Value.Equals(other.Value); } public override bool Equals(object obj) @@ -35,12 +36,12 @@ namespace nadena.dev.modular_avatar.core public override int GetHashCode() { - return HashCode.Combine(ShapeName, (int)ChangeType, Value); + return HashCode.Combine(Object, ShapeName, (int)ChangeType, Value); } public override string ToString() { - return $"{ShapeName} {ChangeType} {Value}"; + return $"{Object.referencePath} {ShapeName} {ChangeType} {Value}"; } } @@ -48,14 +49,10 @@ namespace nadena.dev.modular_avatar.core [HelpURL("https://modular-avatar.nadena.dev/docs/reference/shape-changer?lang=auto")] public class ModularAvatarShapeChanger : ReactiveComponent { - [SerializeField] [FormerlySerializedAs("targetRenderer")] - private AvatarObjectReference m_targetRenderer; - - public AvatarObjectReference targetRenderer - { - get => m_targetRenderer; - set => m_targetRenderer = value; - } + // Migration field to help with 1.10-beta series avatar data. Since this was never in a released version of MA, + // this migration support will be removed in 1.10.0. + [SerializeField] [FormerlySerializedAs("targetRenderer")] [HideInInspector] + private AvatarObjectReference m_targetRenderer = new(); [SerializeField] [FormerlySerializedAs("Shapes")] private List m_shapes = new(); @@ -68,7 +65,44 @@ namespace nadena.dev.modular_avatar.core public override void ResolveReferences() { - m_targetRenderer?.Get(this); + foreach (var shape in m_shapes) + { + shape.Object?.Get(this); + } + } + + private void OnEnable() + { + MigrateTargetRenderer(); + } + + protected override void OnValidate() + { + base.OnValidate(); + MigrateTargetRenderer(); + } + + // Migrate early versions of MASC (from Modular Avatar 1.10.0-beta.4 or earlier) to the new format, where the + // target renderer is stored separately for each shape. + // This logic will be removed in 1.10.0. + private void MigrateTargetRenderer() + { + // Note: This method runs in the context of OnValidate, and therefore cannot touch any other unity objects. + if (!string.IsNullOrEmpty(m_targetRenderer.referencePath) || m_targetRenderer.targetObject != null) + { + foreach (var shape in m_shapes) + { + if (shape.Object == null) shape.Object = new AvatarObjectReference(); + + if (string.IsNullOrEmpty(shape.Object.referencePath) && shape.Object.targetObject == null) + { + shape.Object.referencePath = m_targetRenderer.referencePath; + shape.Object.targetObject = m_targetRenderer.targetObject; + } + } + m_targetRenderer.referencePath = null; + m_targetRenderer.targetObject = null; + } } } } \ No newline at end of file