diff --git a/.github/ProjectRoot/vpm-manifest-2022.json b/.github/ProjectRoot/vpm-manifest-2022.json index dcb09696..f6886049 100644 --- a/.github/ProjectRoot/vpm-manifest-2022.json +++ b/.github/ProjectRoot/vpm-manifest-2022.json @@ -19,7 +19,7 @@ "dependencies": {} }, "nadena.dev.ndmf": { - "version": "1.5.0-beta.5" + "version": "1.5.0-rc.1" } } } \ No newline at end of file diff --git a/Editor/Inspector/Common/LogoElement.cs b/Editor/Inspector/Common/LogoElement.cs index 92029fdc..fc9aefbd 100644 --- a/Editor/Inspector/Common/LogoElement.cs +++ b/Editor/Inspector/Common/LogoElement.cs @@ -20,6 +20,14 @@ namespace nadena.dev.modular_avatar.core.editor private VisualElement _inner; + public new class UxmlFactory : UxmlFactory + { + } + + public new class UxmlTraits : VisualElement.UxmlTraits + { + } + private static void RegisterNode(LogoElement target) { if (_logoDisplayNode == null) diff --git a/Editor/Inspector/Common/LogoImage.cs b/Editor/Inspector/Common/LogoImage.cs index b569671d..e1ac0278 100644 --- a/Editor/Inspector/Common/LogoImage.cs +++ b/Editor/Inspector/Common/LogoImage.cs @@ -1,5 +1,6 @@ #region +using UnityEditor; using UnityEngine.UIElements; #endregion @@ -28,11 +29,26 @@ namespace nadena.dev.modular_avatar.core.editor var image = new Image(); image.image = LogoDisplay.LOGO_ASSET; - image.style.width = new Length(LogoDisplay.ImageWidth(LogoDisplay.TARGET_HEIGHT), LengthUnit.Pixel); - image.style.height = new Length(LogoDisplay.TARGET_HEIGHT, LengthUnit.Pixel); + + SetImageSize(image); _inner.Add(image); Add(_inner); } + + private static void SetImageSize(Image image, int maxTries = 10) + { + var targetHeight = LogoDisplay.TARGET_HEIGHT; + + if (targetHeight == 0) + { + if (maxTries <= 0) return; + EditorApplication.delayCall += () => SetImageSize(image, maxTries - 1); + targetHeight = 45; + } + + image.style.width = new Length(LogoDisplay.ImageWidth(targetHeight), LengthUnit.Pixel); + image.style.height = new Length(targetHeight, LengthUnit.Pixel); + } } } \ No newline at end of file diff --git a/Editor/Inspector/LogoDisplay.cs b/Editor/Inspector/LogoDisplay.cs index 60473f9b..c583b297 100644 --- a/Editor/Inspector/LogoDisplay.cs +++ b/Editor/Inspector/LogoDisplay.cs @@ -11,7 +11,20 @@ namespace nadena.dev.modular_avatar.core.editor internal static class LogoDisplay { internal static readonly Texture2D LOGO_ASSET; - internal static float TARGET_HEIGHT => EditorStyles.label.lineHeight * 3; + internal static float TARGET_HEIGHT + { + get { + try + { + return (EditorStyles.label?.lineHeight ?? 0) * 3; + } + catch (NullReferenceException e) + { + // This can happen in early initialization... + return 0; + } + } + } internal static float ImageWidth(float height) { diff --git a/Editor/Inspector/MaterialSetter/MaterialSetter.uxml b/Editor/Inspector/MaterialSetter/MaterialSetter.uxml index ea0ee768..3260d366 100644 --- a/Editor/Inspector/MaterialSetter/MaterialSetter.uxml +++ b/Editor/Inspector/MaterialSetter/MaterialSetter.uxml @@ -19,6 +19,7 @@ + \ No newline at end of file diff --git a/Editor/Inspector/MaterialSetter/MaterialSetterEditor.cs b/Editor/Inspector/MaterialSetter/MaterialSetterEditor.cs index 3c371448..d13532c4 100644 --- a/Editor/Inspector/MaterialSetter/MaterialSetterEditor.cs +++ b/Editor/Inspector/MaterialSetter/MaterialSetterEditor.cs @@ -29,6 +29,8 @@ namespace nadena.dev.modular_avatar.core.editor.ShapeChanger root.styleSheets.Add(uss); root.Bind(serializedObject); + + ROSimulatorButton.BindRefObject(root, target); var listView = root.Q("Shapes"); diff --git a/Editor/Inspector/ObjectToggle/ObjectSwitcher.uxml b/Editor/Inspector/ObjectToggle/ObjectSwitcher.uxml index a1771d76..f3607386 100644 --- a/Editor/Inspector/ObjectToggle/ObjectSwitcher.uxml +++ b/Editor/Inspector/ObjectToggle/ObjectSwitcher.uxml @@ -18,7 +18,8 @@ /> - + + \ No newline at end of file diff --git a/Editor/Inspector/ObjectToggle/ObjectSwitcherEditor.cs b/Editor/Inspector/ObjectToggle/ObjectSwitcherEditor.cs index 4c231c0d..e954dda7 100644 --- a/Editor/Inspector/ObjectToggle/ObjectSwitcherEditor.cs +++ b/Editor/Inspector/ObjectToggle/ObjectSwitcherEditor.cs @@ -29,6 +29,8 @@ namespace nadena.dev.modular_avatar.core.editor.ShapeChanger root.styleSheets.Add(uss); root.Bind(serializedObject); + + ROSimulatorButton.BindRefObject(root, target); var listView = root.Q("Shapes"); diff --git a/Editor/Inspector/ShapeChanger/ShapeChanger.uxml b/Editor/Inspector/ShapeChanger/ShapeChanger.uxml index 90e0c8e2..c9754415 100644 --- a/Editor/Inspector/ShapeChanger/ShapeChanger.uxml +++ b/Editor/Inspector/ShapeChanger/ShapeChanger.uxml @@ -19,6 +19,7 @@ + \ No newline at end of file diff --git a/Editor/Inspector/ShapeChanger/ShapeChangerEditor.cs b/Editor/Inspector/ShapeChanger/ShapeChangerEditor.cs index ea0fcdf7..31fc18e4 100644 --- a/Editor/Inspector/ShapeChanger/ShapeChangerEditor.cs +++ b/Editor/Inspector/ShapeChanger/ShapeChangerEditor.cs @@ -33,6 +33,8 @@ namespace nadena.dev.modular_avatar.core.editor.ShapeChanger root.styleSheets.Add(uss); root.Bind(serializedObject); + + ROSimulatorButton.BindRefObject(root, target); var listView = root.Q("Shapes"); diff --git a/Editor/Localization/en-US.json b/Editor/Localization/en-US.json index 4040d9a2..63e3e35b 100644 --- a/Editor/Localization/en-US.json +++ b/Editor/Localization/en-US.json @@ -251,5 +251,32 @@ "ma_info.param_usage_ui.no_data": "[ NO DATA ]", "reactive_object.inverse": "Inverse Condition", "reactive_object.material-setter.set-to": "Set material to: ", - "menuitem.misc.add_toggle": "Add toggle" + "menuitem.misc.add_toggle": "Add toggle", + + "ro_sim.open_debugger_button": "Open reaction debugger", + "ro_sim.window.title": "MA Reaction Debugger", + + "ro_sim.header.inspecting": "Inspecting object", + "ro_sim.header.clear_overrides": "Clear all overrides", + "ro_sim.header.object_state": "Object state", + "ro_sim.state.active": "ACTIVE", + "ro_sim.state.inactive": "INACTIVE", + "ro_sim.header.override_gameobject_state": "Override GameObject state", + "ro_sim.header.override_menuitem_state": "Override MenuItem state", + "ro_sim.affected_by.title": "Affected by:", + + "ro_sim.effect_group.component": "Reactive Component", + "ro_sim.effect_group.controls_obj_state": "Controls object state:", + "ro_sim.effect_group.target_component": "Target Component", + "ro_sim.effect_group.target_component.tooltip": "The component that will be affected by the Reactive Object", + "ro_sim.effect_group.property": "Property", + "ro_sim.effect_group.property.tooltip": "The property of the target component that will be affected by the Reactive Object", + "ro_sim.effect_group.value": "Value", + "ro_sim.effect_group.value.tooltip": "The value that the property will be set to when the Reactive Object is active", + "ro_sim.effect_group.material": "Material", + "ro_sim.effect_group.material.tooltip": "The material to set on the target component when the Reactive Object is active", + + "ro_sim.effect_group.rule_inverted": "This rule is inverted", + "ro_sim.effect_group.rule_inverted.tooltip": "This rule will be applied when one of its conditions is NOT met", + "ro_sim.effect_group.conditions": "Conditions" } \ No newline at end of file diff --git a/Editor/Localization/ja-JP.json b/Editor/Localization/ja-JP.json index e56d18f5..53f842a3 100644 --- a/Editor/Localization/ja-JP.json +++ b/Editor/Localization/ja-JP.json @@ -247,5 +247,32 @@ "ma_info.param_usage_ui.no_data": "[ NO DATA ]", "reactive_object.inverse": "条件を反転", "reactive_object.material-setter.set-to": "変更先のマテリアル ", - "menuitem.misc.add_toggle": "トグルを追加" + "menuitem.misc.add_toggle": "トグルを追加", + + "ro_sim.open_debugger_button": "リアクションデバッグツールを開く", + "ro_sim.window.title": "MA リアクションデバッグツール", + + "ro_sim.header.inspecting": "表示中のオブジェクト", + "ro_sim.header.clear_overrides": "すべてのオーバーライドを解除", + "ro_sim.header.object_state": "オブジェクトのアクティブ状態", + "ro_sim.state.active": "アクティブ", + "ro_sim.state.inactive": "非アクティブ", + "ro_sim.header.override_gameobject_state": "GameObject のアクティブ状態をオーバーライド", + "ro_sim.header.override_menuitem_state": "MenuItem の選択状態をオーバーライト", + "ro_sim.affected_by.title": "以下のルールに影響されています", + + "ro_sim.effect_group.component": "Reactive Component", + "ro_sim.effect_group.controls_obj_state": "オブジェクトのアクティブ状態を設定する➡", + "ro_sim.effect_group.target_component": "コンポーネント", + "ro_sim.effect_group.target_component.tooltip": "Reactive Componentに影響されるコンポーネント", + "ro_sim.effect_group.property": "プロパティ", + "ro_sim.effect_group.property.tooltip": "設定されるプロパティ", + "ro_sim.effect_group.value": "値", + "ro_sim.effect_group.value.tooltip": "上記 Reactive Component が活性状態の時に設定される値", + "ro_sim.effect_group.material": "マテリアル", + "ro_sim.effect_group.material.tooltip": "上記 Reactive Component が活性状態の時に設定されるマテリアル", + + "ro_sim.effect_group.rule_inverted": "このルールの条件が反転されています", + "ro_sim.effect_group.rule_inverted.tooltip": "このルールの条件がどれか一つ満たされない時に適用されます", + "ro_sim.effect_group.conditions": "条件" } \ No newline at end of file diff --git a/Editor/ReactiveObjects/AnimationGeneration/ControlCondition.cs b/Editor/ReactiveObjects/AnimationGeneration/ControlCondition.cs index 3dbd46eb..33368ea5 100644 --- a/Editor/ReactiveObjects/AnimationGeneration/ControlCondition.cs +++ b/Editor/ReactiveObjects/AnimationGeneration/ControlCondition.cs @@ -5,7 +5,7 @@ namespace nadena.dev.modular_avatar.core.editor internal class ControlCondition { public string Parameter; - public UnityEngine.Object ControllingObject; + public UnityEngine.Object DebugReference; public string DebugName; public bool IsConstant; diff --git a/Editor/ReactiveObjects/AnimationGeneration/PropCache.cs b/Editor/ReactiveObjects/AnimationGeneration/PropCache.cs new file mode 100644 index 00000000..63fc36ef --- /dev/null +++ b/Editor/ReactiveObjects/AnimationGeneration/PropCache.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using nadena.dev.ndmf.preview; +using UnityEngine; + +namespace nadena.dev.modular_avatar.core.editor +{ + internal class PropCache + { + private class CacheEntry + { + public ComputeContext GenerateContext, ObserverContext; + public PropCache Owner; + public Key Key; + public Value Value; + } + + private readonly Func _operator; + private readonly Func _equalityComparer; + private readonly Dictionary _cache = new(); + + public PropCache(Func operatorFunc, Func equalityComparer = null) + { + _operator = operatorFunc; + _equalityComparer = equalityComparer; + } + + private static void InvalidateEntry(CacheEntry entry) + { + var newGenContext = new ComputeContext("PropCache for key " + entry.Key); + var newValue = entry.Owner._operator(newGenContext, entry.Key); + if (entry.Owner._equalityComparer != null && entry.Owner._equalityComparer(entry.Value, newValue)) + { + entry.GenerateContext = newGenContext; + entry.GenerateContext.InvokeOnInvalidate(entry, InvalidateEntry); + return; + } + + entry.Owner._cache.Remove(entry.Key); + entry.ObserverContext.Invalidate(); + } + + public Value Get(ComputeContext context, Key key) + { + if (!_cache.TryGetValue(key, out var entry) || entry.GenerateContext.IsInvalidated) + { + var subContext = new ComputeContext("PropCache for key " + key); + entry = new CacheEntry + { + GenerateContext = subContext, + ObserverContext = new ComputeContext("Observer for PropCache for key " + key), + Owner = this, + Key = key, + Value = _operator(subContext, key) + }; + _cache[key] = entry; + + subContext.InvokeOnInvalidate(entry, InvalidateEntry); + } + + entry.ObserverContext.Invalidates(context); + + return entry.Value; + } + } +} \ No newline at end of file diff --git a/Editor/ReactiveObjects/AnimationGeneration/PropCache.cs.meta b/Editor/ReactiveObjects/AnimationGeneration/PropCache.cs.meta new file mode 100644 index 00000000..deae7e29 --- /dev/null +++ b/Editor/ReactiveObjects/AnimationGeneration/PropCache.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: d838828e060c4ad7bae36334d868ae36 +timeCreated: 1724624517 \ No newline at end of file diff --git a/Editor/ReactiveObjects/AnimationGeneration/ReactionRule.cs b/Editor/ReactiveObjects/AnimationGeneration/ReactionRule.cs index fe24e1cb..68ae3154 100644 --- a/Editor/ReactiveObjects/AnimationGeneration/ReactionRule.cs +++ b/Editor/ReactiveObjects/AnimationGeneration/ReactionRule.cs @@ -7,52 +7,17 @@ namespace nadena.dev.modular_avatar.core.editor { internal class ReactionRule { - public ReactionRule(ndmf.BuildContext context, TargetProp key, GameObject controllingObject, float value) - : this(context, key, controllingObject, (object)value) { } + public ReactionRule(TargetProp key, float value) + : this(key, (object)value) { } - public ReactionRule(ndmf.BuildContext context, TargetProp key, GameObject controllingObject, UnityEngine.Object value) - : this(context, key, controllingObject, (object)value) { } + public ReactionRule(TargetProp key, UnityEngine.Object value) + : this(key, (object)value) { } - private ReactionRule(ndmf.BuildContext context, TargetProp key, GameObject controllingObject, object value) + private ReactionRule(TargetProp key, object value) { - var asc = context?.Extension(); - TargetProp = key; - var conditions = new List(); - - var cursor = controllingObject?.transform; - - bool did_mami = false; - - while (cursor != null && !RuntimeUtil.IsAvatarRoot(cursor)) - { - // Only look at the menu item closest to the object we're directly attached to, to avoid submenus - // causing issues... - var mami = cursor?.GetComponent(); - if (mami != null && !did_mami) - { - did_mami = true; - - var mami_condition = ParameterAssignerPass.AssignMenuItemParameter(mami); - if (mami_condition != null) conditions.Add(mami_condition); - } - - conditions.Add(new ControlCondition - { - Parameter = asc?.GetActiveSelfProxy(cursor.gameObject) ?? RuntimeUtil.AvatarRootPath(cursor.gameObject), - DebugName = cursor.gameObject.name, - IsConstant = false, - InitialValue = cursor.gameObject.activeSelf ? 1.0f : 0.0f, - ParameterValueLo = 0.5f, - ParameterValueHi = float.PositiveInfinity, - ReferenceObject = cursor.gameObject - }); - - cursor = cursor.parent; - } - - ControllingConditions = conditions; + ControllingConditions = new(); Value = value; } @@ -60,7 +25,9 @@ namespace nadena.dev.modular_avatar.core.editor public TargetProp TargetProp; public object Value; - public readonly List ControllingConditions; + public Component ControllingObject; + + public List ControllingConditions; public bool InitiallyActive => ((ControllingConditions.Count == 0) || ControllingConditions.All(c => c.InitiallyActive)) ^ Inverted; diff --git a/Editor/ReactiveObjects/AnimationGeneration/ReactiveObjectAnalyzer.LocateReactions.cs b/Editor/ReactiveObjects/AnimationGeneration/ReactiveObjectAnalyzer.LocateReactions.cs index 8f6cada2..ea7b4fd6 100644 --- a/Editor/ReactiveObjects/AnimationGeneration/ReactiveObjectAnalyzer.LocateReactions.cs +++ b/Editor/ReactiveObjects/AnimationGeneration/ReactiveObjectAnalyzer.LocateReactions.cs @@ -1,28 +1,111 @@ using System.Collections.Generic; +using System.Linq; using nadena.dev.modular_avatar.animation; +using nadena.dev.ndmf.preview; using UnityEngine; namespace nadena.dev.modular_avatar.core.editor { partial class ReactiveObjectAnalyzer { + private ReactionRule ObjectRule(TargetProp key, Component controllingObject, float value) + { + var rule = new ReactionRule(key, value); + + BuildConditions(controllingObject, rule); + + return rule; + } + + private ReactionRule ObjectRule(TargetProp key, Component controllingObject, UnityEngine.Object value) + { + var rule = new ReactionRule(key, value); + + BuildConditions(controllingObject, rule); + + return rule; + } + + private string GetActiveSelfProxy(GameObject obj) + { + if (_asc != null) + { + return _asc.GetActiveSelfProxy(obj); + } + else + { + var param = "__ActiveSelfProxy/" + obj.GetInstanceID(); + _simulationInitialStates[param] = obj.activeSelf ? 1.0f : 0.0f; + return param; + } + } + private void BuildConditions(Component controllingComponent, ReactionRule rule) + { + rule.ControllingObject = controllingComponent; + + var conditions = new List(); + + var cursor = controllingComponent?.transform; + + bool did_mami = false; + + _computeContext.ObservePath(controllingComponent.transform); + + while (cursor != null && !RuntimeUtil.IsAvatarRoot(cursor)) + { + // Only look at the menu item closest to the object we're directly attached to, to avoid submenus + // causing issues... + var mami = _computeContext.GetComponent(cursor.gameObject); + if (mami != null && !did_mami) + { + did_mami = true; + + _computeContext.Observe(mami, c => (c.Control?.parameter, c.Control?.type, c.Control?.value, c.isDefault)); + + var mami_condition = ParameterAssignerPass.AssignMenuItemParameter(mami, _simulationInitialStates); + if (mami_condition != null) conditions.Add(mami_condition); + } + + conditions.Add(new ControlCondition + { + Parameter = GetActiveSelfProxy(cursor.gameObject), + DebugName = cursor.gameObject.name, + IsConstant = false, + InitialValue = cursor.gameObject.activeSelf ? 1.0f : 0.0f, + ParameterValueLo = 0.5f, + ParameterValueHi = float.PositiveInfinity, + ReferenceObject = cursor.gameObject, + DebugReference = cursor.gameObject, + }); + + cursor = cursor.parent; + } + + rule.ControllingConditions = conditions; + } + private Dictionary FindShapes(GameObject root) { - var changers = root.GetComponentsInChildren(true); + var changers = _computeContext.GetComponentsInChildren(root, true); Dictionary shapeKeys = new(); foreach (var changer in changers) { if (changer.Shapes == null) continue; + var shapes = _computeContext.Observe(changer, + c => c.Shapes.Select(s => s.Clone()).ToList(), + (a,b) => a.SequenceEqual(b) + ); - foreach (var shape in changer.Shapes) + foreach (var shape in shapes) { - var renderer = shape.Object.Get(changer)?.GetComponent(); + var renderer = _computeContext.GetComponent(shape.Object.Get(changer)); if (renderer == null) continue; var mesh = renderer.sharedMesh; + _computeContext.Observe(mesh); if (mesh == null) continue; var shapeId = mesh.GetBlendShapeIndex(shape.ShapeName); @@ -41,12 +124,12 @@ namespace nadena.dev.modular_avatar.core.editor shapeKeys[key] = info; // Add initial state - var agk = new ReactionRule(context, key, null, value); + var agk = new ReactionRule(key, value); agk.Value = renderer.GetBlendShapeWeight(shapeId); info.actionGroups.Add(agk); } - var action = new ReactionRule(context, key, changer.gameObject, value); + var action = ObjectRule(key, changer, value); action.Inverted = changer.Inverted; var isCurrentlyActive = changer.gameObject.activeInHierarchy; @@ -63,20 +146,14 @@ namespace nadena.dev.modular_avatar.core.editor if (changer.gameObject.activeInHierarchy) info.currentState = action.Value; - Debug.Log("Trying merge: " + action); if (info.actionGroups.Count == 0) { info.actionGroups.Add(action); } else if (!info.actionGroups[^1].TryMerge(action)) { - Debug.Log("Failed merge"); info.actionGroups.Add(action); } - else - { - Debug.Log("Post merge: " + info.actionGroups[^1]); - } } } @@ -91,9 +168,9 @@ namespace nadena.dev.modular_avatar.core.editor { if (setter.Objects == null) continue; - foreach (var obj in setter.Objects) + foreach (var obj in _computeContext.Observe(setter, c => c.Objects.ToList(), Enumerable.SequenceEqual)) { - var renderer = obj.Object.Get(setter)?.GetComponent(); + var renderer = _computeContext.GetComponent(obj.Object.Get(setter)); if (renderer == null || renderer.sharedMaterials.Length < obj.MaterialIndex) continue; var key = new TargetProp @@ -108,7 +185,7 @@ namespace nadena.dev.modular_avatar.core.editor objectGroups[key] = group; } - var action = new ReactionRule(context, key, setter.gameObject, obj.Material); + var action = ObjectRule(key, setter, obj.Material); action.Inverted = setter.Inverted; if (group.actionGroups.Count == 0) @@ -126,7 +203,7 @@ namespace nadena.dev.modular_avatar.core.editor { if (toggle.Objects == null) continue; - foreach (var obj in toggle.Objects) + foreach (var obj in _computeContext.Observe(toggle, c => c.Objects.ToList(), Enumerable.SequenceEqual)) { var target = obj.Object.Get(toggle); if (target == null) continue; @@ -144,7 +221,7 @@ namespace nadena.dev.modular_avatar.core.editor } var value = obj.Active ? 1 : 0; - var action = new ReactionRule(context, key, toggle.gameObject, value); + var action = ObjectRule(key, toggle, value); action.Inverted = toggle.Inverted; if (group.actionGroups.Count == 0) diff --git a/Editor/ReactiveObjects/AnimationGeneration/ReactiveObjectAnalyzer.cs b/Editor/ReactiveObjects/AnimationGeneration/ReactiveObjectAnalyzer.cs index 8925853c..1f087949 100644 --- a/Editor/ReactiveObjects/AnimationGeneration/ReactiveObjectAnalyzer.cs +++ b/Editor/ReactiveObjects/AnimationGeneration/ReactiveObjectAnalyzer.cs @@ -1,6 +1,9 @@ using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using nadena.dev.modular_avatar.animation; +using nadena.dev.modular_avatar.core.editor.Simulator; +using nadena.dev.ndmf.preview; using UnityEngine; namespace nadena.dev.modular_avatar.core.editor @@ -11,16 +14,65 @@ namespace nadena.dev.modular_avatar.core.editor /// internal partial class ReactiveObjectAnalyzer { - private readonly ndmf.BuildContext context; + private readonly ComputeContext _computeContext; + private readonly ndmf.BuildContext _context; + private readonly AnimationServicesContext _asc; + private Dictionary _simulationInitialStates; + + public ImmutableDictionary ForcePropertyOverrides { get; set; } = ImmutableDictionary.Empty; public ReactiveObjectAnalyzer(ndmf.BuildContext context) { - this.context = context; + _computeContext = ComputeContext.NullContext; + _context = context; + _asc = context.Extension(); + _simulationInitialStates = null; } - public ReactiveObjectAnalyzer() + public ReactiveObjectAnalyzer(ComputeContext computeContext = null) { - context = null; + _computeContext = computeContext ?? ComputeContext.NullContext; + _context = null; + _asc = null; + _simulationInitialStates = new(); + } + + public string GetGameObjectStateProperty(GameObject obj) + { + return GetActiveSelfProxy(obj); + } + + public string GetMenuItemProperty(GameObject obj) + { + var mami = obj?.GetComponent(); + if (mami == null) return null; + + return ParameterAssignerPass.AssignMenuItemParameter(mami, _simulationInitialStates)?.Parameter; + } + + public struct AnalysisResult + { + public Dictionary Shapes; + public Dictionary InitialStates; + public HashSet DeletedShapes; + } + + private static PropCache _analysisCache; + + public static AnalysisResult CachedAnalyze(ComputeContext context, GameObject root) + { + if (_analysisCache == null) + { + _analysisCache = new PropCache((ctx, root) => + { + var analysis = new ReactiveObjectAnalyzer(ctx); + analysis.ForcePropertyOverrides = ctx.Observe(ROSimulator.PropertyOverrides, a=>a, (a,b) => false) + ?? ImmutableDictionary.Empty; + return analysis.Analyze(root); + }); + } + + return _analysisCache.Get(context, root); } /// @@ -30,21 +82,49 @@ namespace nadena.dev.modular_avatar.core.editor /// A dictionary of target property to initial state (float or UnityEngine.Object) /// A hashset of blendshape properties which are always deleted /// - public Dictionary Analyze( - GameObject root, - out Dictionary initialStates, - out HashSet deletedShapes + public AnalysisResult Analyze( + GameObject root ) { + AnalysisResult result = new(); + + if (root == null) + { + result.Shapes = new(); + result.InitialStates = new(); + result.DeletedShapes = new(); + return result; + } + Dictionary shapes = FindShapes(root); FindObjectToggles(shapes, root); FindMaterialSetters(shapes, root); + ApplyInitialStateOverrides(shapes); AnalyzeConstants(shapes); ResolveToggleInitialStates(shapes); - PreprocessShapes(shapes, out initialStates, out deletedShapes); + PreprocessShapes(shapes, out result.InitialStates, out result.DeletedShapes); + result.Shapes = shapes; - return shapes; + return result; + } + + private void ApplyInitialStateOverrides(Dictionary shapes) + { + foreach (var prop in shapes.Values) + { + foreach (var rule in prop.actionGroups) + { + foreach (var cond in rule.ControllingConditions) + { + var paramName = cond.Parameter; + if (ForcePropertyOverrides.TryGetValue(paramName, out var value)) + { + cond.InitialValue = value; + } + } + } + } } /// @@ -54,7 +134,7 @@ namespace nadena.dev.modular_avatar.core.editor /// private void AnalyzeConstants(Dictionary shapes) { - var asc = context?.Extension(); + var asc = _context?.Extension(); HashSet toggledObjects = new(); if (asc == null) return; @@ -98,11 +178,16 @@ namespace nadena.dev.modular_avatar.core.editor /// private void ResolveToggleInitialStates(Dictionary groups) { - var asc = context?.Extension(); + var asc = _context?.Extension(); - Dictionary propStates = new Dictionary(); - Dictionary nextPropStates = new Dictionary(); + Dictionary propStates = new(); + Dictionary nextPropStates = new(); int loopLimit = 5; + + foreach (var kvp in ForcePropertyOverrides) + { + propStates[kvp.Key] = kvp.Value; + } bool unsettled = true; while (unsettled && loopLimit-- > 0) @@ -114,10 +199,10 @@ namespace nadena.dev.modular_avatar.core.editor if (group.TargetProp.PropertyName != "m_IsActive") continue; if (!(group.TargetProp.TargetObject is GameObject targetObject)) continue; - var pathKey = asc?.GetActiveSelfProxy(targetObject) ?? RuntimeUtil.AvatarRootPath(targetObject); - - bool state; - if (!propStates.TryGetValue(pathKey, out state)) state = targetObject.activeSelf; + var pathKey = GetActiveSelfProxy(targetObject); + + float state; + if (!propStates.TryGetValue(pathKey, out state)) state = targetObject.activeSelf ? 1 : 0; foreach (var actionGroup in group.actionGroups) { @@ -126,10 +211,10 @@ namespace nadena.dev.modular_avatar.core.editor { if (!propStates.TryGetValue(condition.Parameter, out var propCondition)) { - propCondition = condition.InitiallyActive; + propCondition = condition.InitiallyActive ? 1 : 0; } - if (!propCondition) + if (propCondition < 0.5f) { evaluated = false; break; @@ -140,18 +225,23 @@ namespace nadena.dev.modular_avatar.core.editor if (evaluated) { - state = (float) actionGroup.Value > 0.5f; + state = (float) actionGroup.Value; } } nextPropStates[pathKey] = state; - if (!propStates.TryGetValue(pathKey, out var oldState) || oldState != state) + if (!propStates.TryGetValue(pathKey, out var oldState) || !Mathf.Approximately(oldState, state)) { unsettled = true; } } + foreach (var kvp in ForcePropertyOverrides) + { + nextPropStates[kvp.Key] = kvp.Value; + } + propStates = nextPropStates; nextPropStates = new(); } @@ -163,7 +253,7 @@ namespace nadena.dev.modular_avatar.core.editor foreach (var condition in action.ControllingConditions) { if (propStates.TryGetValue(condition.Parameter, out var state)) - condition.InitialValue = state ? 1.0f : 0.0f; + condition.InitialValue = state; } } } diff --git a/Editor/ReactiveObjects/AnimationGeneration/ReactiveObjectPass.cs b/Editor/ReactiveObjects/AnimationGeneration/ReactiveObjectPass.cs index e1489320..ee4d37db 100644 --- a/Editor/ReactiveObjects/AnimationGeneration/ReactiveObjectPass.cs +++ b/Editor/ReactiveObjects/AnimationGeneration/ReactiveObjectPass.cs @@ -33,12 +33,11 @@ namespace nadena.dev.modular_avatar.core.editor internal void Execute() { - Dictionary shapes = - new ReactiveObjectAnalyzer(context).Analyze( - context.AvatarRootObject, - out var initialStates, - out var deletedShapes - ); + var analysis = new ReactiveObjectAnalyzer(context).Analyze(context.AvatarRootObject); + + var shapes = analysis.Shapes; + var initialStates = analysis.InitialStates; + var deletedShapes = analysis.DeletedShapes; GenerateActiveSelfProxies(shapes); diff --git a/Editor/ReactiveObjects/AnimationGeneration/TargetProp.cs b/Editor/ReactiveObjects/AnimationGeneration/TargetProp.cs index e3bd7467..eff80696 100644 --- a/Editor/ReactiveObjects/AnimationGeneration/TargetProp.cs +++ b/Editor/ReactiveObjects/AnimationGeneration/TargetProp.cs @@ -7,6 +7,15 @@ namespace nadena.dev.modular_avatar.core.editor public Object TargetObject; public string PropertyName; + public static TargetProp ForObjectActive(GameObject targetObject) + { + return new TargetProp + { + TargetObject = targetObject, + PropertyName = "m_IsActive" + }; + } + public bool Equals(TargetProp other) { return Equals(TargetObject, other.TargetObject) && PropertyName == other.PropertyName; diff --git a/Editor/ReactiveObjects/MaterialSetterPreview.cs b/Editor/ReactiveObjects/MaterialSetterPreview.cs index fef718e9..3c1e9dae 100644 --- a/Editor/ReactiveObjects/MaterialSetterPreview.cs +++ b/Editor/ReactiveObjects/MaterialSetterPreview.cs @@ -25,115 +25,104 @@ namespace nadena.dev.modular_avatar.core.editor return context.Observe(EnableNode.IsEnabled); } + private const string PREFIX = "m_Materials.Array.data["; + + private PropCache> _cache = new( + GetMaterialOverridesForRenderer, Enumerable.SequenceEqual + ); + + private static ImmutableList<(int, Material)> GetMaterialOverridesForRenderer(ComputeContext ctx, Renderer r) + { + var avatar = ctx.GetAvatarRoot(r.gameObject); + var analysis = ReactiveObjectAnalyzer.CachedAnalyze(ctx, avatar); + + var materials = ImmutableList<(int, Material)>.Empty; + + foreach (var prop in analysis.Shapes.Values) + { + var target = prop.TargetProp; + if (target.TargetObject != r) continue; + if (!target.PropertyName.StartsWith(PREFIX)) continue; + + var index = int.Parse(target.PropertyName.Substring(PREFIX.Length, target.PropertyName.IndexOf(']') - PREFIX.Length)); + + var activeRule = prop.actionGroups.FirstOrDefault(r => r.InitiallyActive); + if (activeRule == null || activeRule.Value is not Material mat) continue; + + materials = materials.Add((index, mat)); + } + + return materials.OrderBy(kv => kv.Item1).ToImmutableList(); + } + + private IEnumerable GroupsForAvatar(ComputeContext context, GameObject avatarRoot) + { + var analysis = ReactiveObjectAnalyzer.CachedAnalyze(context, avatarRoot); + + HashSet renderers = new(); + + foreach (var prop in analysis.Shapes.Values) + { + var target = prop.TargetProp; + if (target.TargetObject is not Renderer r || r == null) continue; + if (!target.PropertyName.StartsWith(PREFIX)) continue; + + var index = int.Parse(target.PropertyName.Substring(PREFIX.Length, target.PropertyName.IndexOf(']') - PREFIX.Length)); + + var activeRule = prop.actionGroups.FirstOrDefault(r => r.InitiallyActive); + if (activeRule == null || activeRule.Value is not Material mat) continue; + + renderers.Add(r); + } + + return renderers.Select(RenderGroup.For); + } public ImmutableList GetTargetGroups(ComputeContext context) { - var menuItemPreview = new MenuItemPreviewCondition(context); - var setters = context.GetComponentsByType(); - - 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, s => s.Inverted)) continue; - - var objects = context.Observe(setter, s => s.Objects.Select(o => (o.Object.Get(s), o.Material, o.MaterialIndex)).ToList(), Enumerable.SequenceEqual); - - foreach (var (target, mat, index) in objects) - { - var renderer = context.GetComponent(target); - if (renderer == null) continue; - if (renderer is not MeshRenderer and not SkinnedMeshRenderer) continue; - - var matCount = context.Observe(renderer, r => r.sharedMaterials.Length); - - if (matCount <= index) continue; - - if (!builders.TryGetValue(renderer, out var builder)) - { - builder = ImmutableList.CreateBuilder(); - builders[renderer] = builder; - } - - builder.Add(setter); - } - } - - return builders.Select(g => RenderGroup.For(g.Key).WithData(g.Value.ToImmutable())) - .ToImmutableList(); + var avatarRoots = context.GetAvatarRoots(); + return avatarRoots.SelectMany(r => GroupsForAvatar(context, r)).ToImmutableList(); } public Task Instantiate(RenderGroup group, IEnumerable<(Renderer, Renderer)> proxyPairs, ComputeContext context) { - var setters = group.GetData>(); - var node = new Node(setters); + var node = new Node(_cache, proxyPairs.First().Item1); - return node.Refresh(proxyPairs, context, 0); + return node.Refresh(null, context, 0); } private class Node : IRenderFilterNode { - private readonly ImmutableList _setters; - private ImmutableList<(int, Material)> _materials; + private readonly Renderer _target; + private readonly PropCache> _cache; + private ImmutableList<(int, Material)> _materials = ImmutableList<(int, Material)>.Empty; - public RenderAspects WhatChanged => RenderAspects.Material; + public RenderAspects WhatChanged { get; private set; } = RenderAspects.Material; - public Node(ImmutableList setters) + public Node(PropCache> cache, Renderer renderer) { - _setters = setters; - _materials = ImmutableList<(int, Material)>.Empty; + _cache = cache; + _target = renderer; } public Task Refresh(IEnumerable<(Renderer, Renderer)> proxyPairs, ComputeContext context, RenderAspects updatedAspects) { - var (original, proxy) = proxyPairs.First(); + var newMaterials = _cache.Get(context, _target); - if (original == null || proxy == null) return null; - if (original is not MeshRenderer and not SkinnedMeshRenderer || proxy is not MeshRenderer and not SkinnedMeshRenderer) return null; - - var mats = new Material[proxy.sharedMaterials.Length]; - - foreach (var setter in _setters) + if (newMaterials.SequenceEqual(_materials)) { - 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 (target, mat, index) in objects) - { - var renderer = context.GetComponent(target); - if (renderer != original) continue; - - if (index <= mats.Length) - { - mats[index] = mat; - } - } + WhatChanged = 0; + } else { + _materials = newMaterials; + WhatChanged = RenderAspects.Material; } - var materials = mats.Select((m, i) => (i, m)).Where(kvp => kvp.m != null).ToImmutableList(); - - if (!materials.SequenceEqual(_materials)) - { - return Task.FromResult(new Node(_setters) - { - _materials = materials, - }); - } - return Task.FromResult(this); } public void OnFrame(Renderer original, Renderer proxy) { if (original == null || proxy == null) return; - if (original is not MeshRenderer and not SkinnedMeshRenderer || proxy is not MeshRenderer and not SkinnedMeshRenderer) return; var mats = proxy.sharedMaterials; diff --git a/Editor/ReactiveObjects/ObjectTogglePreview.cs b/Editor/ReactiveObjects/ObjectTogglePreview.cs index b5c20ed1..4612a0be 100644 --- a/Editor/ReactiveObjects/ObjectTogglePreview.cs +++ b/Editor/ReactiveObjects/ObjectTogglePreview.cs @@ -2,6 +2,7 @@ using System.Collections.Immutable; using System.Linq; using System.Threading.Tasks; +using nadena.dev.modular_avatar.core.editor.Simulator; using nadena.dev.ndmf.preview; using UnityEngine; @@ -27,99 +28,55 @@ namespace nadena.dev.modular_avatar.core.editor return context.Observe(EnableNode.IsEnabled); } + private IEnumerable RootsForAvatar(ComputeContext context, GameObject avatarRoot) + { + if (!context.ActiveInHierarchy(avatarRoot)) + { + yield break; + } + + var analysis = ReactiveObjectAnalyzer.CachedAnalyze(context, avatarRoot); + var initialStates = analysis.InitialStates; + + var renderers = context.GetComponentsInChildren(avatarRoot, true); + + foreach (var renderer in renderers) + { + bool currentlyEnabled = context.ActiveInHierarchy(renderer.gameObject); + + bool overrideEnabled = true; + Transform cursor = renderer.transform; + while (cursor != null && !RuntimeUtil.IsAvatarRoot(cursor)) + { + if (initialStates.TryGetValue(TargetProp.ForObjectActive(cursor.gameObject), out var initialState) && initialState is float f) + { + if (f < 0.5f) + { + overrideEnabled = false; + break; + } + } + else if (!cursor.gameObject.activeSelf) + { + overrideEnabled = false; + break; + } + + cursor = cursor.parent; + } + + if (overrideEnabled != currentlyEnabled) + { + yield return RenderGroup.For(renderer).WithData(overrideEnabled); + } + } + } + public ImmutableList GetTargetGroups(ComputeContext context) { - var menuItemPreview = new MenuItemPreviewCondition(context); - var allToggles = context.GetComponentsByType(); - - var objectGroups = - new Dictionary.Builder>( - new ObjectIdentityComparer()); - - foreach (var toggle in allToggles) - { - var mami = context.GetComponent(toggle.gameObject); - - bool active = context.ActiveAndEnabled(toggle) && (mami == null || menuItemPreview.IsEnabledForPreview(mami)); - if (active == context.Observe(toggle, t => t.Inverted)) continue; - - context.Observe(toggle, - t => t.Objects.Select(o => o.Object.referencePath).ToList(), - (x, y) => x.SequenceEqual(y) - ); - - if (toggle.Objects == null) continue; - - var index = -1; - foreach (var switched in toggle.Objects) - { - index++; - - if (switched.Object == null) continue; - - var target = context.Observe(toggle, _ => switched.Object.Get(toggle)); - - if (target == null) continue; - - if (!objectGroups.TryGetValue(target, out var group)) - { - group = ImmutableList.CreateBuilder<(ModularAvatarObjectToggle, int)>(); - objectGroups[target] = group; - } - - group.Add((toggle, index)); - } - } - - var affectedRenderers = objectGroups.Keys - .SelectMany(go => context.GetComponentsInChildren(go, true)) - // If we have overlapping objects, we need to sort by child to parent, so parent configuration overrides - // the child. We do this by simply looking at how many times we observe each renderer. - .GroupBy(r => r) - .Select(g => g.Key) - .ToHashSet(); - - var renderGroups = new List(); + var roots = context.GetAvatarRoots(); - foreach (var r in affectedRenderers) - { - var shouldEnable = true; - - var obj = r.gameObject; - context.ActiveInHierarchy(obj); // observe path changes & object state changes - - while (obj != null) - { - var enableAtNode = obj.activeSelf; - - var group = objectGroups.GetValueOrDefault(obj); - if (group == null && !obj.activeSelf) - { - // always inactive - shouldEnable = false; - break; - } - - if (group != null) - { - var (toggle, index) = group[^1]; - enableAtNode = context.Observe(toggle, t => t.Objects.Count > index && t.Objects[index].Active); - } - - if (!enableAtNode) - { - shouldEnable = false; - break; - } - - obj = obj.transform.parent?.gameObject; - } - - if (shouldEnable != r.gameObject.activeInHierarchy) - renderGroups.Add(RenderGroup.For(r).WithData(shouldEnable)); - } - - return renderGroups.ToImmutableList(); + return roots.SelectMany(av => RootsForAvatar(context, av)).ToImmutableList(); } public Task Instantiate(RenderGroup group, IEnumerable<(Renderer, Renderer)> proxyPairs, diff --git a/Editor/ReactiveObjects/ParameterAssignerPass.cs b/Editor/ReactiveObjects/ParameterAssignerPass.cs index bb9c6363..e654828f 100644 --- a/Editor/ReactiveObjects/ParameterAssignerPass.cs +++ b/Editor/ReactiveObjects/ParameterAssignerPass.cs @@ -118,18 +118,33 @@ namespace nadena.dev.modular_avatar.core.editor } } - internal static ControlCondition AssignMenuItemParameter(ModularAvatarMenuItem mami) + internal static ControlCondition AssignMenuItemParameter(ModularAvatarMenuItem mami, Dictionary simulationInitialStates = null) { - if (string.IsNullOrWhiteSpace(mami?.Control?.parameter?.name)) return null; + var paramName = mami?.Control?.parameter?.name; + if (mami?.Control != null && simulationInitialStates != null && ShouldAssignParametersToMami(mami)) + { + paramName = "___AutoProp/" + mami.Control?.parameter?.name; + + if (mami.isDefault) + { + simulationInitialStates[paramName] = mami.Control.value; + } else if (!simulationInitialStates.ContainsKey(paramName)) + { + simulationInitialStates[paramName] = -999; + } + } + + if (string.IsNullOrWhiteSpace(paramName)) return null; return new ControlCondition { - Parameter = mami.Control.parameter.name, + Parameter = paramName, DebugName = mami.gameObject.name, IsConstant = false, InitialValue = mami.isDefault ? mami.Control.value : -999, // TODO ParameterValueLo = mami.Control.value - 0.5f, - ParameterValueHi = mami.Control.value + 0.5f + ParameterValueHi = mami.Control.value + 0.5f, + DebugReference = mami, }; } } diff --git a/Editor/ReactiveObjects/ShapeChangerPreview.cs b/Editor/ReactiveObjects/ShapeChangerPreview.cs index ba862cf4..e26e48aa 100644 --- a/Editor/ReactiveObjects/ShapeChangerPreview.cs +++ b/Editor/ReactiveObjects/ShapeChangerPreview.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Threading.Tasks; +using nadena.dev.modular_avatar.core.editor.Simulator; using nadena.dev.ndmf.preview; using UnityEngine; using Object = UnityEngine.Object; @@ -30,173 +31,159 @@ namespace nadena.dev.modular_avatar.core.editor { return context.Observe(EnableNode.IsEnabled); } + + private class StaticContext + { + public StaticContext(GameObject avatarRoot, IEnumerable shapes) + { + AvatarRoot = avatarRoot; + Shapes = shapes.OrderBy(i => i).ToImmutableList(); + } + + public GameObject AvatarRoot { get; } + public ImmutableList Shapes { get; } + + public override bool Equals(object obj) + { + return obj is StaticContext other && Shapes.SequenceEqual(other.Shapes) && AvatarRoot == other.AvatarRoot; + } + + public override int GetHashCode() + { + int hash = AvatarRoot.GetHashCode(); + foreach (var shape in Shapes) + { + hash = hash * 31 + shape.GetHashCode(); + } + + return hash; + } + } + + private PropCache>> + _blendshapeCache = new(ShapesForAvatar); + + private static ImmutableDictionary> ShapesForAvatar(ComputeContext context, GameObject avatarRoot) + { + if (avatarRoot == null || !context.ActiveInHierarchy(avatarRoot)) + { + return ImmutableDictionary>.Empty; + } + + var analysis = ReactiveObjectAnalyzer.CachedAnalyze(context, avatarRoot); + var shapes = analysis.Shapes; + + ImmutableDictionary>.Builder rendererStates = + ImmutableDictionary.CreateBuilder>( + new ObjectIdentityComparer() + ); + var avatarRootTransform = avatarRoot.transform; + + foreach (var prop in shapes.Values) + { + var target = prop.TargetProp; + if (target.TargetObject == null || target.TargetObject is not SkinnedMeshRenderer r) continue; + if (!r.transform.IsChildOf(avatarRootTransform)) continue; + if (!target.PropertyName.StartsWith("blendShape.")) continue; + + var mesh = r.sharedMesh; + if (mesh == null) continue; + + var shapeName = target.PropertyName.Substring("blendShape.".Length); + + if (!rendererStates.TryGetValue(r, out var states)) + { + states = ImmutableList<(int, float)>.Empty; + rendererStates[r] = states; + } + + var index = r.sharedMesh.GetBlendShapeIndex(shapeName); + if (index < 0) continue; + + var activeRule = prop.actionGroups.LastOrDefault(rule => rule.InitiallyActive); + if (activeRule == null || activeRule.Value is not float value) continue; + + value = Math.Clamp(value, 0, 100); + + if (activeRule.IsDelete) value = -1; + + states = states.Add((index, value)); + rendererStates[r] = states; + } + + return rendererStates.ToImmutableDictionary(); + } + + private IEnumerable ShapesToGroups(GameObject avatarRoot, ImmutableDictionary> shapes) + { + return shapes.Select(kv => RenderGroup.For(kv.Key).WithData( + new StaticContext(avatarRoot, kv.Value.Where(shape => shape.Item2 < 0).Select(shape => shape.Item1)) + )); + } public ImmutableList GetTargetGroups(ComputeContext context) { - var menuItemPreview = new MenuItemPreviewCondition(context); - var changers = context.GetComponentsByType(); - - var builders = - new Dictionary.Builder>( - new ObjectIdentityComparer()); - - foreach (var changer in changers) - { - if (changer == null) 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 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 renderer = context.GetComponent(target); - if (renderer == null) continue; - - if (!builders.TryGetValue(renderer, out var builder)) - { - builder = ImmutableList.CreateBuilder(); - builders[renderer] = builder; - } - - builder.Add(changer); - } - } + var roots = context.GetAvatarRoots(); - return builders.Select(g => RenderGroup.For(g.Key).WithData(g.Value.ToImmutable())) + + return roots + .SelectMany(av => + ShapesToGroups(av, _blendshapeCache.Get(context, av)) + ) .ToImmutableList(); } public Task Instantiate(RenderGroup group, IEnumerable<(Renderer, Renderer)> proxyPairs, ComputeContext context) { - var changers = group.GetData>(); - var node = new Node(changers); + var shapeValues = group.GetData(); + var node = new Node(shapeValues, proxyPairs.First().Item2 as SkinnedMeshRenderer, _blendshapeCache); return node.Refresh(proxyPairs, context, 0); } private class Node : IRenderFilterNode { - private readonly ImmutableList _changers; - private ImmutableHashSet<(int, float)> _shapes; + private readonly PropCache>> _blendshapeCache; + private readonly GameObject _avatarRoot; + private ImmutableList<(int, float)> _shapes; private ImmutableHashSet _toDelete; private Mesh _generatedMesh = null; public RenderAspects WhatChanged => RenderAspects.Shapes | RenderAspects.Mesh; - internal Node(ImmutableList changers) + internal Node(StaticContext staticContext, SkinnedMeshRenderer proxySmr, PropCache>> blendshapeCache) { - _changers = changers; - _shapes = ImmutableHashSet<(int, float)>.Empty; - _toDelete = ImmutableHashSet.Empty; - _generatedMesh = null; + _blendshapeCache = blendshapeCache; + _avatarRoot = staticContext.AvatarRoot; + _toDelete = staticContext.Shapes.ToImmutableHashSet(); + _shapes = ImmutableList<(int, float)>.Empty; + _generatedMesh = GetGeneratedMesh(proxySmr, _toDelete); } public Task Refresh(IEnumerable<(Renderer, Renderer)> proxyPairs, ComputeContext context, RenderAspects updatedAspects) { - 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)) + if ((updatedAspects & RenderAspects.Mesh) != 0) { - return Task.FromResult(new Node(_changers) - { - _shapes = shapes, - _toDelete = toDelete, - _generatedMesh = GetGeneratedMesh(proxySmr, toDelete), - }); + return Task.FromResult(null); } - if (!shapes.SequenceEqual(_shapes)) + var avatarInfo = _blendshapeCache.Get(context, _avatarRoot); + if (!avatarInfo.TryGetValue((SkinnedMeshRenderer)proxyPairs.First().Item1, out var shapes)) { - var reusableMesh = _generatedMesh; - _generatedMesh = null; - return Task.FromResult(new Node(_changers) - { - _shapes = shapes, - _toDelete = toDelete, - _generatedMesh = reusableMesh, - }); + return Task.FromResult(null); } + var toDelete = shapes.Where(shape => shape.Item2 < 0).Select(shape => shape.Item1).ToImmutableHashSet(); + if (!_toDelete.SetEquals(toDelete)) + { + return Task.FromResult(null); + } + + _shapes = shapes; + 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) - { - Debug.Log($"mesh changed {a.GetInstanceID()} -> {b.GetInstanceID()}"); - return false; - } - - return true; - }); - - foreach (var changer in _changers) - { - if (changer == null) continue; - - 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 renderer = context.GetComponent(target); - if (renderer != original) continue; - - var index = mesh.GetBlendShapeIndex(name); - if (index < 0) continue; - builder.Add((index, type == ShapeChangeType.Delete ? 100 : value)); - } - } - - return builder.ToImmutable(); - } - - private ImmutableHashSet GetToDeleteSet(SkinnedMeshRenderer original, SkinnedMeshRenderer proxy, ComputeContext context) - { - var builder = ImmutableHashSet.CreateBuilder(); - var mesh = context.Observe(proxy, p => p.sharedMesh, (a, b) => - { - 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); - } - } - - return builder.ToImmutable(); - } - public Mesh GetGeneratedMesh(SkinnedMeshRenderer proxy, ImmutableHashSet toDelete) { var mesh = proxy.sharedMesh; @@ -263,7 +250,7 @@ namespace nadena.dev.modular_avatar.core.editor foreach (var shape in _shapes) { - proxySmr.SetBlendShapeWeight(shape.Item1, shape.Item2); + proxySmr.SetBlendShapeWeight(shape.Item1, shape.Item2 < 0 ? 100 : shape.Item2); } } diff --git a/Editor/ReactiveObjects/Simulator.meta b/Editor/ReactiveObjects/Simulator.meta new file mode 100644 index 00000000..19c57b26 --- /dev/null +++ b/Editor/ReactiveObjects/Simulator.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 8df71f6c94c04c82b246b7cc8c1e3ef3 +timeCreated: 1724533011 \ No newline at end of file diff --git a/Editor/ReactiveObjects/Simulator/EffectGroup.uxml b/Editor/ReactiveObjects/Simulator/EffectGroup.uxml new file mode 100644 index 00000000..1b999153 --- /dev/null +++ b/Editor/ReactiveObjects/Simulator/EffectGroup.uxml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Editor/ReactiveObjects/Simulator/EffectGroup.uxml.meta b/Editor/ReactiveObjects/Simulator/EffectGroup.uxml.meta new file mode 100644 index 00000000..3fbbecaf --- /dev/null +++ b/Editor/ReactiveObjects/Simulator/EffectGroup.uxml.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 9af042eb03914091ba0152eb8e67ab26 +timeCreated: 1724539594 \ No newline at end of file diff --git a/Editor/ReactiveObjects/Simulator/ROSimulator.cs b/Editor/ReactiveObjects/Simulator/ROSimulator.cs new file mode 100644 index 00000000..59819dae --- /dev/null +++ b/Editor/ReactiveObjects/Simulator/ROSimulator.cs @@ -0,0 +1,516 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using nadena.dev.modular_avatar.ui; +using nadena.dev.ndmf.localization; +using nadena.dev.ndmf.preview; +using UnityEditor; +using UnityEditor.UIElements; +using UnityEngine; +using UnityEngine.UIElements; + +namespace nadena.dev.modular_avatar.core.editor.Simulator +{ + internal class ROSimulator : EditorWindow, IHasCustomMenu + { + public static PublishedValue> PropertyOverrides = new(null); + + internal static string ROOT_PATH = "Packages/nadena.dev.modular-avatar/Editor/ReactiveObjects/Simulator/"; + private static string USS = ROOT_PATH + "ROSimulator.uss"; + private static string UXML = ROOT_PATH + "ROSimulator.uxml"; + private static string EFFECT_GROUP_UXML = ROOT_PATH + "EffectGroup.uxml"; + + private ObjectField f_inspecting; + private VisualElement e_debugInfo; + private VisualTreeAsset effectGroupTemplate; + private StyleSheet uss; + + [MenuItem(UnityMenuItems.GameObject_ShowReactionDebugger, false, UnityMenuItems.GameObject_ShowReactionDebuggerOrder)] + internal static void ShowWindow(MenuCommand command) + { + OpenDebugger(command.context as GameObject); + } + + private void Awake() + { + void SetTitle(EditorWindow w) + { + w.titleContent = new GUIContent(Localization.L.GetLocalizedString("ro_sim.window.title")); + } + + LanguagePrefs.RegisterLanguageChangeCallback(this, SetTitle); + SetTitle(this); + } + + public static void OpenDebugger(GameObject target) + { + var window = GetWindow(); + if (window.is_enabled && window.locked) return; + + window.locked = target != Selection.activeGameObject; + + // avoid racing with initial creation + if (window.f_inspecting == null) + { + window.LoadUI(); + } + + // ReSharper disable once PossibleNullReferenceException + window.f_inspecting.SetValueWithoutNotify(target); + window.RefreshUI(); + } + + private void OnEnable() + { + PropertyOverrides.Value = ImmutableDictionary.Empty; + EditorApplication.delayCall += LoadUI; + Selection.selectionChanged += SelectionChanged; + is_enabled = true; + } + + private void OnDisable() + { + Selection.selectionChanged -= SelectionChanged; + is_enabled = false; + + PropertyOverrides.Value = null; + } + + private ComputeContext _lastComputeContext; + private GameObject currentSelection; + private GUIStyle lockButtonStyle; + private bool locked, is_enabled; + + private Dictionary<(int, string), bool> foldoutState = new(); + private Button _btn_clear; + + private void UpdatePropertyOverride(string prop, bool? enable, float f_val) + { + if (enable == null) + { + PropertyOverrides.Value = PropertyOverrides.Value.Remove(prop); + } else if (enable.Value) + { + PropertyOverrides.Value = PropertyOverrides.Value.SetItem(prop, f_val); + } + else + { + PropertyOverrides.Value = PropertyOverrides.Value.SetItem(prop, 0f); + } + + EditorApplication.delayCall += RefreshUI; + } + + private void UpdatePropertyOverride(string prop, bool? value) + { + if (value == null) + { + PropertyOverrides.Value = PropertyOverrides.Value.Remove(prop); + } + else + { + PropertyOverrides.Value = PropertyOverrides.Value.SetItem(prop, value.Value ? 1f : 0f); + } + + RefreshUI(); + } + + private void ShowButton(Rect rect) + { + if (lockButtonStyle == null) + { + lockButtonStyle = "IN LockButton"; + } + + locked = GUI.Toggle(rect, locked, GUIContent.none, lockButtonStyle); + } + + void IHasCustomMenu.AddItemsToMenu(GenericMenu menu) + { + menu.AddItem(new GUIContent("Lock"), locked, () => locked = !locked); + } + + void SelectionChanged() + { + if (locked) return; + + if (currentSelection != Selection.activeGameObject) + { + UpdateSelection(); + } + } + + private void LoadUI() + { + var root = rootVisualElement; + root.Clear(); + root.AddToClassList("rootVisualContent"); + uss = AssetDatabase.LoadAssetAtPath(USS); + + root.styleSheets.Add(uss); + var content = AssetDatabase.LoadAssetAtPath(UXML).CloneTree(); + root.Add(content); + + Localization.L.LocalizeUIElements(content); + + root.Q