mirror of
https://github.com/bdunderscore/modular-avatar.git
synced 2025-01-30 10:12:59 +08:00
feat: add a debugging UI for the reactive components system (#1049)
This commit is contained in:
parent
07660164ba
commit
87a385a43e
2
.github/ProjectRoot/vpm-manifest-2022.json
vendored
2
.github/ProjectRoot/vpm-manifest-2022.json
vendored
@ -19,7 +19,7 @@
|
||||
"dependencies": {}
|
||||
},
|
||||
"nadena.dev.ndmf": {
|
||||
"version": "1.5.0-beta.5"
|
||||
"version": "1.5.0-rc.1"
|
||||
}
|
||||
}
|
||||
}
|
@ -20,6 +20,14 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
|
||||
private VisualElement _inner;
|
||||
|
||||
public new class UxmlFactory : UxmlFactory<LogoElement, UxmlTraits>
|
||||
{
|
||||
}
|
||||
|
||||
public new class UxmlTraits : VisualElement.UxmlTraits
|
||||
{
|
||||
}
|
||||
|
||||
private static void RegisterNode(LogoElement target)
|
||||
{
|
||||
if (_logoDisplayNode == null)
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
{
|
||||
|
@ -19,6 +19,7 @@
|
||||
</ui:VisualElement>
|
||||
</ui:VisualElement>
|
||||
|
||||
<ma:ROSimulatorButton/>
|
||||
<ma:LanguageSwitcherElement/>
|
||||
</ui:VisualElement>
|
||||
</UXML>
|
@ -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<ListView>("Shapes");
|
||||
|
||||
|
@ -18,7 +18,8 @@
|
||||
/>
|
||||
</ui:VisualElement>
|
||||
</ui:VisualElement>
|
||||
|
||||
|
||||
<ma:ROSimulatorButton/>
|
||||
<ma:LanguageSwitcherElement/>
|
||||
</ui:VisualElement>
|
||||
</UXML>
|
@ -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<ListView>("Shapes");
|
||||
|
||||
|
@ -19,6 +19,7 @@
|
||||
</ui:VisualElement>
|
||||
</ui:VisualElement>
|
||||
|
||||
<ma:ROSimulatorButton/>
|
||||
<ma:LanguageSwitcherElement/>
|
||||
</ui:VisualElement>
|
||||
</UXML>
|
@ -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<ListView>("Shapes");
|
||||
|
||||
|
@ -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"
|
||||
}
|
@ -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": "条件"
|
||||
}
|
@ -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;
|
||||
|
66
Editor/ReactiveObjects/AnimationGeneration/PropCache.cs
Normal file
66
Editor/ReactiveObjects/AnimationGeneration/PropCache.cs
Normal file
@ -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<Key, Value>
|
||||
{
|
||||
private class CacheEntry
|
||||
{
|
||||
public ComputeContext GenerateContext, ObserverContext;
|
||||
public PropCache<Key, Value> Owner;
|
||||
public Key Key;
|
||||
public Value Value;
|
||||
}
|
||||
|
||||
private readonly Func<ComputeContext, Key, Value> _operator;
|
||||
private readonly Func<Value, Value, bool> _equalityComparer;
|
||||
private readonly Dictionary<Key, CacheEntry> _cache = new();
|
||||
|
||||
public PropCache(Func<ComputeContext, Key, Value> operatorFunc, Func<Value, Value, bool> 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d838828e060c4ad7bae36334d868ae36
|
||||
timeCreated: 1724624517
|
@ -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<AnimationServicesContext>();
|
||||
|
||||
TargetProp = key;
|
||||
|
||||
var conditions = new List<ControlCondition>();
|
||||
|
||||
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<ModularAvatarMenuItem>();
|
||||
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<ControlCondition> ControllingConditions;
|
||||
public Component ControllingObject;
|
||||
|
||||
public List<ControlCondition> ControllingConditions;
|
||||
|
||||
public bool InitiallyActive =>
|
||||
((ControllingConditions.Count == 0) || ControllingConditions.All(c => c.InitiallyActive)) ^ Inverted;
|
||||
|
@ -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<ControlCondition>();
|
||||
|
||||
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<ModularAvatarMenuItem>(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<TargetProp, AnimatedProperty> FindShapes(GameObject root)
|
||||
{
|
||||
var changers = root.GetComponentsInChildren<ModularAvatarShapeChanger>(true);
|
||||
var changers = _computeContext.GetComponentsInChildren<ModularAvatarShapeChanger>(root, true);
|
||||
|
||||
Dictionary<TargetProp, AnimatedProperty> 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<SkinnedMeshRenderer>();
|
||||
var renderer = _computeContext.GetComponent<SkinnedMeshRenderer>(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<Renderer>();
|
||||
var renderer = _computeContext.GetComponent<Renderer>(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)
|
||||
|
@ -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
|
||||
/// </summary>
|
||||
internal partial class ReactiveObjectAnalyzer
|
||||
{
|
||||
private readonly ndmf.BuildContext context;
|
||||
private readonly ComputeContext _computeContext;
|
||||
private readonly ndmf.BuildContext _context;
|
||||
private readonly AnimationServicesContext _asc;
|
||||
private Dictionary<string, float> _simulationInitialStates;
|
||||
|
||||
public ImmutableDictionary<string, float> ForcePropertyOverrides { get; set; } = ImmutableDictionary<string, float>.Empty;
|
||||
|
||||
public ReactiveObjectAnalyzer(ndmf.BuildContext context)
|
||||
{
|
||||
this.context = context;
|
||||
_computeContext = ComputeContext.NullContext;
|
||||
_context = context;
|
||||
_asc = context.Extension<AnimationServicesContext>();
|
||||
_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<ModularAvatarMenuItem>();
|
||||
if (mami == null) return null;
|
||||
|
||||
return ParameterAssignerPass.AssignMenuItemParameter(mami, _simulationInitialStates)?.Parameter;
|
||||
}
|
||||
|
||||
public struct AnalysisResult
|
||||
{
|
||||
public Dictionary<TargetProp, AnimatedProperty> Shapes;
|
||||
public Dictionary<TargetProp, object> InitialStates;
|
||||
public HashSet<TargetProp> DeletedShapes;
|
||||
}
|
||||
|
||||
private static PropCache<GameObject, AnalysisResult> _analysisCache;
|
||||
|
||||
public static AnalysisResult CachedAnalyze(ComputeContext context, GameObject root)
|
||||
{
|
||||
if (_analysisCache == null)
|
||||
{
|
||||
_analysisCache = new PropCache<GameObject, AnalysisResult>((ctx, root) =>
|
||||
{
|
||||
var analysis = new ReactiveObjectAnalyzer(ctx);
|
||||
analysis.ForcePropertyOverrides = ctx.Observe(ROSimulator.PropertyOverrides, a=>a, (a,b) => false)
|
||||
?? ImmutableDictionary<string, float>.Empty;
|
||||
return analysis.Analyze(root);
|
||||
});
|
||||
}
|
||||
|
||||
return _analysisCache.Get(context, root);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -30,21 +82,49 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
/// <param name="initialStates">A dictionary of target property to initial state (float or UnityEngine.Object)</param>
|
||||
/// <param name="deletedShapes">A hashset of blendshape properties which are always deleted</param>
|
||||
/// <returns></returns>
|
||||
public Dictionary<TargetProp, AnimatedProperty> Analyze(
|
||||
GameObject root,
|
||||
out Dictionary<TargetProp, object> initialStates,
|
||||
out HashSet<TargetProp> deletedShapes
|
||||
public AnalysisResult Analyze(
|
||||
GameObject root
|
||||
)
|
||||
{
|
||||
AnalysisResult result = new();
|
||||
|
||||
if (root == null)
|
||||
{
|
||||
result.Shapes = new();
|
||||
result.InitialStates = new();
|
||||
result.DeletedShapes = new();
|
||||
return result;
|
||||
}
|
||||
|
||||
Dictionary<TargetProp, AnimatedProperty> 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<TargetProp, AnimatedProperty> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -54,7 +134,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
/// <param name="shapes"></param>
|
||||
private void AnalyzeConstants(Dictionary<TargetProp, AnimatedProperty> shapes)
|
||||
{
|
||||
var asc = context?.Extension<AnimationServicesContext>();
|
||||
var asc = _context?.Extension<AnimationServicesContext>();
|
||||
HashSet<GameObject> toggledObjects = new();
|
||||
|
||||
if (asc == null) return;
|
||||
@ -98,11 +178,16 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
/// <param name="groups"></param>
|
||||
private void ResolveToggleInitialStates(Dictionary<TargetProp, AnimatedProperty> groups)
|
||||
{
|
||||
var asc = context?.Extension<AnimationServicesContext>();
|
||||
var asc = _context?.Extension<AnimationServicesContext>();
|
||||
|
||||
Dictionary<string, bool> propStates = new Dictionary<string, bool>();
|
||||
Dictionary<string, bool> nextPropStates = new Dictionary<string, bool>();
|
||||
Dictionary<string, float> propStates = new();
|
||||
Dictionary<string, float> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -33,12 +33,11 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
|
||||
internal void Execute()
|
||||
{
|
||||
Dictionary<TargetProp, AnimatedProperty> 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);
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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<Renderer, ImmutableList<(int, Material)>> _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<RenderGroup> GroupsForAvatar(ComputeContext context, GameObject avatarRoot)
|
||||
{
|
||||
var analysis = ReactiveObjectAnalyzer.CachedAnalyze(context, avatarRoot);
|
||||
|
||||
HashSet<Renderer> 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<RenderGroup> GetTargetGroups(ComputeContext context)
|
||||
{
|
||||
var menuItemPreview = new MenuItemPreviewCondition(context);
|
||||
var setters = context.GetComponentsByType<ModularAvatarMaterialSetter>();
|
||||
|
||||
var builders =
|
||||
new Dictionary<Renderer, ImmutableList<ModularAvatarMaterialSetter>.Builder>(
|
||||
new ObjectIdentityComparer<Renderer>());
|
||||
|
||||
foreach (var setter in setters)
|
||||
{
|
||||
if (setter == null) continue;
|
||||
|
||||
var mami = context.GetComponent<ModularAvatarMenuItem>(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<Renderer>(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<ModularAvatarMaterialSetter>();
|
||||
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<IRenderFilterNode> Instantiate(RenderGroup group, IEnumerable<(Renderer, Renderer)> proxyPairs, ComputeContext context)
|
||||
{
|
||||
var setters = group.GetData<ImmutableList<ModularAvatarMaterialSetter>>();
|
||||
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<ModularAvatarMaterialSetter> _setters;
|
||||
private ImmutableList<(int, Material)> _materials;
|
||||
private readonly Renderer _target;
|
||||
private readonly PropCache<Renderer, ImmutableList<(int, Material)>> _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<ModularAvatarMaterialSetter> setters)
|
||||
public Node(PropCache<Renderer, ImmutableList<(int, Material)>> cache, Renderer renderer)
|
||||
{
|
||||
_setters = setters;
|
||||
_materials = ImmutableList<(int, Material)>.Empty;
|
||||
_cache = cache;
|
||||
_target = renderer;
|
||||
}
|
||||
|
||||
public Task<IRenderFilterNode> 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<Renderer>(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<IRenderFilterNode>(new Node(_setters)
|
||||
{
|
||||
_materials = materials,
|
||||
});
|
||||
}
|
||||
|
||||
return Task.FromResult<IRenderFilterNode>(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;
|
||||
|
||||
|
@ -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<RenderGroup> 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<Renderer>(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<RenderGroup> GetTargetGroups(ComputeContext context)
|
||||
{
|
||||
var menuItemPreview = new MenuItemPreviewCondition(context);
|
||||
var allToggles = context.GetComponentsByType<ModularAvatarObjectToggle>();
|
||||
|
||||
var objectGroups =
|
||||
new Dictionary<GameObject, ImmutableList<(ModularAvatarObjectToggle, int)>.Builder>(
|
||||
new ObjectIdentityComparer<GameObject>());
|
||||
|
||||
foreach (var toggle in allToggles)
|
||||
{
|
||||
var mami = context.GetComponent<ModularAvatarMenuItem>(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<Renderer>(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<RenderGroup>();
|
||||
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<IRenderFilterNode> Instantiate(RenderGroup group, IEnumerable<(Renderer, Renderer)> proxyPairs,
|
||||
|
@ -118,18 +118,33 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
}
|
||||
}
|
||||
|
||||
internal static ControlCondition AssignMenuItemParameter(ModularAvatarMenuItem mami)
|
||||
internal static ControlCondition AssignMenuItemParameter(ModularAvatarMenuItem mami, Dictionary<string, float> 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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<int> shapes)
|
||||
{
|
||||
AvatarRoot = avatarRoot;
|
||||
Shapes = shapes.OrderBy(i => i).ToImmutableList();
|
||||
}
|
||||
|
||||
public GameObject AvatarRoot { get; }
|
||||
public ImmutableList<int> 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<GameObject, ImmutableDictionary<SkinnedMeshRenderer, ImmutableList<(int, float)>>>
|
||||
_blendshapeCache = new(ShapesForAvatar);
|
||||
|
||||
private static ImmutableDictionary<SkinnedMeshRenderer, ImmutableList<(int, float)>> ShapesForAvatar(ComputeContext context, GameObject avatarRoot)
|
||||
{
|
||||
if (avatarRoot == null || !context.ActiveInHierarchy(avatarRoot))
|
||||
{
|
||||
return ImmutableDictionary<SkinnedMeshRenderer, ImmutableList<(int, float)>>.Empty;
|
||||
}
|
||||
|
||||
var analysis = ReactiveObjectAnalyzer.CachedAnalyze(context, avatarRoot);
|
||||
var shapes = analysis.Shapes;
|
||||
|
||||
ImmutableDictionary<SkinnedMeshRenderer, ImmutableList<(int, float)>>.Builder rendererStates =
|
||||
ImmutableDictionary.CreateBuilder<SkinnedMeshRenderer, ImmutableList<(int, float)>>(
|
||||
new ObjectIdentityComparer<SkinnedMeshRenderer>()
|
||||
);
|
||||
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<RenderGroup> ShapesToGroups(GameObject avatarRoot, ImmutableDictionary<SkinnedMeshRenderer, ImmutableList<(int, float)>> 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<RenderGroup> GetTargetGroups(ComputeContext context)
|
||||
{
|
||||
var menuItemPreview = new MenuItemPreviewCondition(context);
|
||||
var changers = context.GetComponentsByType<ModularAvatarShapeChanger>();
|
||||
|
||||
var builders =
|
||||
new Dictionary<Renderer, ImmutableList<ModularAvatarShapeChanger>.Builder>(
|
||||
new ObjectIdentityComparer<Renderer>());
|
||||
|
||||
foreach (var changer in changers)
|
||||
{
|
||||
if (changer == null) continue;
|
||||
|
||||
var mami = context.GetComponent<ModularAvatarMenuItem>(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<SkinnedMeshRenderer>(target);
|
||||
if (renderer == null) continue;
|
||||
|
||||
if (!builders.TryGetValue(renderer, out var builder))
|
||||
{
|
||||
builder = ImmutableList.CreateBuilder<ModularAvatarShapeChanger>();
|
||||
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<IRenderFilterNode> Instantiate(RenderGroup group, IEnumerable<(Renderer, Renderer)> proxyPairs, ComputeContext context)
|
||||
{
|
||||
var changers = group.GetData<ImmutableList<ModularAvatarShapeChanger>>();
|
||||
var node = new Node(changers);
|
||||
var shapeValues = group.GetData<StaticContext>();
|
||||
var node = new Node(shapeValues, proxyPairs.First().Item2 as SkinnedMeshRenderer, _blendshapeCache);
|
||||
|
||||
return node.Refresh(proxyPairs, context, 0);
|
||||
}
|
||||
|
||||
private class Node : IRenderFilterNode
|
||||
{
|
||||
private readonly ImmutableList<ModularAvatarShapeChanger> _changers;
|
||||
private ImmutableHashSet<(int, float)> _shapes;
|
||||
private readonly PropCache<GameObject, ImmutableDictionary<SkinnedMeshRenderer, ImmutableList<(int, float)>>> _blendshapeCache;
|
||||
private readonly GameObject _avatarRoot;
|
||||
private ImmutableList<(int, float)> _shapes;
|
||||
private ImmutableHashSet<int> _toDelete;
|
||||
private Mesh _generatedMesh = null;
|
||||
|
||||
public RenderAspects WhatChanged => RenderAspects.Shapes | RenderAspects.Mesh;
|
||||
|
||||
internal Node(ImmutableList<ModularAvatarShapeChanger> changers)
|
||||
internal Node(StaticContext staticContext, SkinnedMeshRenderer proxySmr, PropCache<GameObject, ImmutableDictionary<SkinnedMeshRenderer, ImmutableList<(int, float)>>> blendshapeCache)
|
||||
{
|
||||
_changers = changers;
|
||||
_shapes = ImmutableHashSet<(int, float)>.Empty;
|
||||
_toDelete = ImmutableHashSet<int>.Empty;
|
||||
_generatedMesh = null;
|
||||
_blendshapeCache = blendshapeCache;
|
||||
_avatarRoot = staticContext.AvatarRoot;
|
||||
_toDelete = staticContext.Shapes.ToImmutableHashSet();
|
||||
_shapes = ImmutableList<(int, float)>.Empty;
|
||||
_generatedMesh = GetGeneratedMesh(proxySmr, _toDelete);
|
||||
}
|
||||
|
||||
public Task<IRenderFilterNode> 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<IRenderFilterNode>(new Node(_changers)
|
||||
{
|
||||
_shapes = shapes,
|
||||
_toDelete = toDelete,
|
||||
_generatedMesh = GetGeneratedMesh(proxySmr, toDelete),
|
||||
});
|
||||
return Task.FromResult<IRenderFilterNode>(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<IRenderFilterNode>(new Node(_changers)
|
||||
{
|
||||
_shapes = shapes,
|
||||
_toDelete = toDelete,
|
||||
_generatedMesh = reusableMesh,
|
||||
});
|
||||
return Task.FromResult<IRenderFilterNode>(null);
|
||||
}
|
||||
|
||||
var toDelete = shapes.Where(shape => shape.Item2 < 0).Select(shape => shape.Item1).ToImmutableHashSet();
|
||||
if (!_toDelete.SetEquals(toDelete))
|
||||
{
|
||||
return Task.FromResult<IRenderFilterNode>(null);
|
||||
}
|
||||
|
||||
_shapes = shapes;
|
||||
|
||||
return Task.FromResult<IRenderFilterNode>(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<SkinnedMeshRenderer>(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<int> GetToDeleteSet(SkinnedMeshRenderer original, SkinnedMeshRenderer proxy, ComputeContext context)
|
||||
{
|
||||
var builder = ImmutableHashSet.CreateBuilder<int>();
|
||||
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<SkinnedMeshRenderer>(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<int> 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
3
Editor/ReactiveObjects/Simulator.meta
Normal file
3
Editor/ReactiveObjects/Simulator.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8df71f6c94c04c82b246b7cc8c1e3ef3
|
||||
timeCreated: 1724533011
|
33
Editor/ReactiveObjects/Simulator/EffectGroup.uxml
Normal file
33
Editor/ReactiveObjects/Simulator/EffectGroup.uxml
Normal file
@ -0,0 +1,33 @@
|
||||
<UXML xmlns:ui="UnityEngine.UIElements" xmlns:ed="UnityEditor.UIElements"
|
||||
xmlns:ma="nadena.dev.modular_avatar.core.editor">
|
||||
<ui:VisualElement class="effect-group">
|
||||
<ed:ObjectField label="Reactive Component" name="effect__source"/>
|
||||
|
||||
<ui:VisualElement name="effect__set-active" class="unity-base-field h-group">
|
||||
<ui:Label text="ro_sim.effect_group.controls_obj_state" class="unity-base-field__label ndmf-tr"/>
|
||||
<ui:Label text="ro_sim.state.active" class="st-enabled ndmf-tr"/>
|
||||
</ui:VisualElement>
|
||||
|
||||
<ui:VisualElement name="effect__set-inactive" class="unity-base-field h-group">
|
||||
<ui:Label text="ro_sim.effect_group.controls_obj_state" class="unity-base-field__label ndmf-tr"/>
|
||||
<ui:Label text="ro_sim.state.inactive" class="st-disabled ndmf-tr"/>
|
||||
</ui:VisualElement>
|
||||
|
||||
<ed:ObjectField label="ro_sim.effect_group.target_component" name="effect__target" class="ndmf-tr"/>
|
||||
<ui:TextField label="ro_sim.effect_group.property" text="blendShapes.x" name="effect__prop" class="ndmf-tr-label"/>
|
||||
<ed:FloatField label="ro_sim.effect_group.value" name="effect__value" class="ndmf-tr-label"/>
|
||||
<ed:ObjectField label="ro_sim.effect_group.material" name="effect__material" class="ndmf-tr"/>
|
||||
<ui:TextField label="ro_sim.effect_group.value" text="DELETE" name="effect__deleted" class="ndmf-tr-label"/>
|
||||
|
||||
<ui:VisualElement name="controlling-conditions">
|
||||
<ui:Label text="ro_sim.state.active" class="st-enabled when-active state-header ndmf-tr"/>
|
||||
<ui:Label text="ro_sim.state.inactive" class="st-disabled when-inactive state-header ndmf-tr"/>
|
||||
<ui:HelpBox text="ro_sim.effect_group.rule_inverted" message-type="Warning" class="when-inverted ndmf-tr"/>
|
||||
|
||||
<ui:VisualElement class="spacer"/>
|
||||
<ui:Label text="ro_sim.effect_group.conditions" class="heading ndmf-tr"/>
|
||||
|
||||
</ui:VisualElement>
|
||||
</ui:VisualElement>
|
||||
|
||||
</UXML>
|
3
Editor/ReactiveObjects/Simulator/EffectGroup.uxml.meta
Normal file
3
Editor/ReactiveObjects/Simulator/EffectGroup.uxml.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9af042eb03914091ba0152eb8e67ab26
|
||||
timeCreated: 1724539594
|
516
Editor/ReactiveObjects/Simulator/ROSimulator.cs
Normal file
516
Editor/ReactiveObjects/Simulator/ROSimulator.cs
Normal file
@ -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<ImmutableDictionary<string, float>> 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<ROSimulator>();
|
||||
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<string, float>.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<StyleSheet>(USS);
|
||||
|
||||
root.styleSheets.Add(uss);
|
||||
var content = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(UXML).CloneTree();
|
||||
root.Add(content);
|
||||
|
||||
Localization.L.LocalizeUIElements(content);
|
||||
|
||||
root.Q<Button>("debug__reload").clickable.clicked += LoadUI;
|
||||
|
||||
f_inspecting = root.Q<ObjectField>("inspecting");
|
||||
f_inspecting.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
locked = true;
|
||||
UpdateSelection();
|
||||
});
|
||||
|
||||
_btn_clear = root.Q<Button>("clear-overrides");
|
||||
_btn_clear.clickable.clicked += () =>
|
||||
{
|
||||
PropertyOverrides.Value = ImmutableDictionary<string, float>.Empty;
|
||||
RefreshUI();
|
||||
};
|
||||
|
||||
e_debugInfo = root.Q<VisualElement>("debug-info");
|
||||
|
||||
effectGroupTemplate = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(EFFECT_GROUP_UXML);
|
||||
|
||||
UpdateSelection();
|
||||
}
|
||||
|
||||
private void UpdateSelection()
|
||||
{
|
||||
currentSelection = locked ? f_inspecting.value as GameObject : Selection.activeGameObject;
|
||||
f_inspecting.SetValueWithoutNotify(currentSelection);
|
||||
|
||||
RefreshUI();
|
||||
}
|
||||
|
||||
private void RefreshUI()
|
||||
{
|
||||
var avatar = RuntimeUtil.FindAvatarInParents(currentSelection?.transform);
|
||||
|
||||
if (avatar == null)
|
||||
{
|
||||
e_debugInfo.style.display = DisplayStyle.None;
|
||||
return;
|
||||
}
|
||||
|
||||
_btn_clear.SetEnabled(!PropertyOverrides.Value.IsEmpty);
|
||||
|
||||
e_debugInfo.style.display = DisplayStyle.Flex;
|
||||
|
||||
_lastComputeContext = new ComputeContext("RO Simulator");
|
||||
_lastComputeContext.InvokeOnInvalidate(this, MaybeRefreshUI);
|
||||
|
||||
var analysis = new ReactiveObjectAnalyzer(_lastComputeContext);
|
||||
analysis.ForcePropertyOverrides = PropertyOverrides.Value;
|
||||
var result = analysis.Analyze(avatar.gameObject);
|
||||
|
||||
SetThisObjectOverrides(analysis);
|
||||
SetOverallActiveHeader(currentSelection, result.InitialStates);
|
||||
SetAffectedBy(currentSelection, result.Shapes);
|
||||
}
|
||||
|
||||
private static void MaybeRefreshUI(ROSimulator self)
|
||||
{
|
||||
if (self.is_enabled)
|
||||
{
|
||||
self.RefreshUI();
|
||||
}
|
||||
}
|
||||
|
||||
private void SetThisObjectOverrides(ReactiveObjectAnalyzer analysis)
|
||||
{
|
||||
BindOverrideToParameter("this-obj-override", analysis.GetGameObjectStateProperty(currentSelection), 1);
|
||||
BindOverrideToParameter("this-menu-override", analysis.GetMenuItemProperty(currentSelection),
|
||||
currentSelection.GetComponent<ModularAvatarMenuItem>()?.Control?.value ?? 1
|
||||
);
|
||||
}
|
||||
|
||||
private void BindOverrideToParameter(string overrideElemName, string property, float targetValue)
|
||||
{
|
||||
var elem = e_debugInfo.Q<VisualElement>(overrideElemName);
|
||||
var soc = elem.Q<StateOverrideController>();
|
||||
|
||||
if (property == null)
|
||||
{
|
||||
elem.style.display = DisplayStyle.None;
|
||||
return;
|
||||
}
|
||||
elem.style.display = DisplayStyle.Flex;
|
||||
|
||||
if (PropertyOverrides.Value.TryGetValue(property, out var overrideValue))
|
||||
{
|
||||
soc.SetWithoutNotify(Mathf.Approximately(overrideValue, targetValue));
|
||||
}
|
||||
else
|
||||
{
|
||||
soc.SetWithoutNotify(null);
|
||||
}
|
||||
|
||||
soc.OnStateOverrideChanged += value =>
|
||||
{
|
||||
UpdatePropertyOverride(property, value, targetValue);
|
||||
};
|
||||
}
|
||||
|
||||
private void SetAffectedBy(GameObject gameObject, Dictionary<TargetProp, AnimatedProperty> shapes)
|
||||
{
|
||||
var effect_list = e_debugInfo.Q<ScrollView>("effect-list");
|
||||
effect_list.Clear();
|
||||
|
||||
var orderedShapes = shapes.Values
|
||||
.Where(s => AffectedBy(s.TargetProp, gameObject))
|
||||
.OrderBy(
|
||||
s =>
|
||||
{
|
||||
if (s.TargetProp.TargetObject is GameObject go && s.TargetProp.PropertyName == "m_IsActive")
|
||||
return (0, null, null);
|
||||
return (1, s.TargetProp.TargetObject.GetType().ToString(), s.TargetProp.PropertyName);
|
||||
}
|
||||
);
|
||||
|
||||
foreach (var shape in orderedShapes)
|
||||
{
|
||||
var targetProp = shape.TargetProp;
|
||||
var propInfo = shape;
|
||||
|
||||
var propGroup = new Foldout();
|
||||
propGroup.text = targetProp.TargetObject.GetType() + "." + targetProp.PropertyName;
|
||||
var foldoutStateKey = (shape.TargetProp.TargetObject?.GetInstanceID() ?? -1, shape.TargetProp.PropertyName);
|
||||
propGroup.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
foldoutState[foldoutStateKey] = evt.newValue;
|
||||
if (evt.newValue)
|
||||
{
|
||||
propGroup.AddToClassList("foldout-open");
|
||||
}
|
||||
else
|
||||
{
|
||||
propGroup.RemoveFromClassList("foldout-open");
|
||||
}
|
||||
});
|
||||
if (shape.TargetProp.TargetObject is GameObject go && shape.TargetProp.PropertyName == "m_IsActive")
|
||||
{
|
||||
propGroup.text = "Active State";
|
||||
propGroup.value = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
propGroup.value = false;
|
||||
}
|
||||
effect_list.Add(propGroup);
|
||||
if (foldoutState.TryGetValue(foldoutStateKey, out var state))
|
||||
{
|
||||
propGroup.value = state;
|
||||
}
|
||||
if (propGroup.value)
|
||||
{
|
||||
propGroup.AddToClassList("foldout-open");
|
||||
}
|
||||
else
|
||||
{
|
||||
propGroup.RemoveFromClassList("foldout-open");
|
||||
}
|
||||
|
||||
foreach (var reactionRule in propInfo.actionGroups)
|
||||
{
|
||||
if (reactionRule.ControllingObject == null) continue;
|
||||
|
||||
var effectGroup = effectGroupTemplate.CloneTree();
|
||||
propGroup.Add(effectGroup);
|
||||
effectGroup.styleSheets.Add(uss);
|
||||
|
||||
if (reactionRule.InitiallyActive)
|
||||
{
|
||||
effectGroup.AddToClassList("st-active");
|
||||
}
|
||||
|
||||
var source = effectGroup.Q<ObjectField>("effect__source");
|
||||
source.SetEnabled(false);
|
||||
source.SetValueWithoutNotify(reactionRule.ControllingObject);
|
||||
|
||||
var conditions = effectGroup.Q<VisualElement>("controlling-conditions");
|
||||
BuildRuleConditionBlock(conditions, reactionRule);
|
||||
|
||||
// For our TextFields, we want to localize the label, not the text, so we'll do this manually...
|
||||
foreach (var field in effectGroup.Query<VisualElement>(classes:"ndmf-tr-label").ToList())
|
||||
{
|
||||
var labelProp = field.GetType().GetProperty("label");
|
||||
var tooltipProp = field.GetType().GetProperty("tooltip");
|
||||
|
||||
if (labelProp == null) continue;
|
||||
|
||||
var key = labelProp.GetValue(field) as string;
|
||||
Action<VisualElement> relocalize = f2 =>
|
||||
{
|
||||
labelProp.SetValue(f2, Localization.L.GetLocalizedString(key));
|
||||
tooltipProp?.SetValue(f2, Localization.L.GetLocalizedString(key + ".tooltip"));
|
||||
};
|
||||
|
||||
relocalize(field);
|
||||
LanguagePrefs.RegisterLanguageChangeCallback(field, relocalize);
|
||||
|
||||
field.RemoveFromClassList("ndmf-tr-label");
|
||||
}
|
||||
|
||||
// Localize once we've built the condition blocks as they contain dynamically created translated
|
||||
// strings.
|
||||
Localization.L.LocalizeUIElements(effectGroup);
|
||||
|
||||
var f_target_component = effectGroup.Q<ObjectField>("effect__target");
|
||||
var f_property = effectGroup.Q<TextField>("effect__prop");
|
||||
var f_set_active = effectGroup.Q<VisualElement>("effect__set-active");
|
||||
var f_set_inactive = effectGroup.Q<VisualElement>("effect__set-inactive");
|
||||
var f_value = effectGroup.Q<FloatField>("effect__value");
|
||||
var f_material = effectGroup.Q<ObjectField>("effect__material");
|
||||
var f_delete = effectGroup.Q("effect__deleted");
|
||||
|
||||
f_target_component.style.display = DisplayStyle.None;
|
||||
f_target_component.SetEnabled(false);
|
||||
f_property.style.display = DisplayStyle.None;
|
||||
f_property.SetEnabled(false);
|
||||
f_set_active.style.display = DisplayStyle.None;
|
||||
f_set_inactive.style.display = DisplayStyle.None;
|
||||
f_value.style.display = DisplayStyle.None;
|
||||
f_value.SetEnabled(false);
|
||||
f_material.style.display = DisplayStyle.None;
|
||||
f_material.SetEnabled(false);
|
||||
f_delete.style.display = DisplayStyle.None;
|
||||
f_delete.SetEnabled(false);
|
||||
|
||||
if (targetProp.TargetObject is GameObject && targetProp.PropertyName == "m_IsActive")
|
||||
{
|
||||
if (((float)reactionRule.Value) > 0.5f)
|
||||
{
|
||||
f_set_active.style.display = DisplayStyle.Flex;
|
||||
}
|
||||
else
|
||||
{
|
||||
f_set_inactive.style.display = DisplayStyle.Flex;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
f_target_component.SetValueWithoutNotify(targetProp.TargetObject);
|
||||
f_target_component.style.display = DisplayStyle.Flex;
|
||||
f_property.value = targetProp.PropertyName;
|
||||
f_property.style.display = DisplayStyle.Flex;
|
||||
|
||||
if (reactionRule.IsDelete)
|
||||
{
|
||||
f_delete.style.display = DisplayStyle.Flex;
|
||||
} else if (reactionRule.Value is float f)
|
||||
{
|
||||
f_value.SetValueWithoutNotify(f);
|
||||
f_value.style.display = DisplayStyle.Flex;
|
||||
} else if (reactionRule.Value is Material m)
|
||||
{
|
||||
f_material.SetValueWithoutNotify(m);
|
||||
f_material.style.display = DisplayStyle.Flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool AffectedBy(TargetProp shapeKey, GameObject gameObject)
|
||||
{
|
||||
return (shapeKey.TargetObject == gameObject) ||
|
||||
(shapeKey.TargetObject is Component c && c.gameObject == gameObject);
|
||||
}
|
||||
|
||||
private void BuildRuleConditionBlock(VisualElement conditions, ReactionRule rule)
|
||||
{
|
||||
if (rule.Inverted)
|
||||
{
|
||||
conditions.AddToClassList("rule-inverted");
|
||||
}
|
||||
|
||||
foreach (var condition in rule.ControllingConditions)
|
||||
{
|
||||
var conditionElem = new VisualElement();
|
||||
conditions.Add(conditionElem);
|
||||
conditionElem.AddToClassList("controlling-condition");
|
||||
|
||||
var soc = new StateOverrideController();
|
||||
conditionElem.Add(soc);
|
||||
|
||||
var prop = condition.Parameter;
|
||||
if (PropertyOverrides.Value.TryGetValue(prop, out var overrideValue))
|
||||
{
|
||||
soc.SetWithoutNotify(condition, overrideValue);
|
||||
}
|
||||
|
||||
float targetValue;
|
||||
|
||||
if (!float.IsFinite(condition.ParameterValueHi))
|
||||
{
|
||||
targetValue = Mathf.Round(condition.ParameterValueLo + 0.5f);
|
||||
} else if (!float.IsFinite(condition.ParameterValueLo))
|
||||
{
|
||||
targetValue = Mathf.Round(condition.ParameterValueHi - 0.5f);
|
||||
}
|
||||
else
|
||||
{
|
||||
targetValue = Mathf.Round((condition.ParameterValueLo + condition.ParameterValueHi) / 2);
|
||||
}
|
||||
|
||||
soc.OnStateOverrideChanged += value => UpdatePropertyOverride(prop, value, targetValue);
|
||||
|
||||
var active = condition.InitiallyActive;
|
||||
var active_label = active ? "active" : "inactive";
|
||||
active_label = "ro_sim.state." + active_label;
|
||||
var active_classname = active ? "source-active" : "source-inactive";
|
||||
|
||||
switch (condition.DebugReference)
|
||||
{
|
||||
case GameObject go:
|
||||
{
|
||||
var controller = new ObjectField(active_label);
|
||||
controller.SetEnabled(false);
|
||||
controller.SetValueWithoutNotify(go);
|
||||
controller.AddToClassList(active_classname);
|
||||
controller.AddToClassList("ndmf-tr");
|
||||
conditionElem.Add(controller);
|
||||
break;
|
||||
}
|
||||
case Component c:
|
||||
{
|
||||
var controller = new ObjectField(active_label);
|
||||
controller.SetEnabled(false);
|
||||
controller.SetValueWithoutNotify(c);
|
||||
controller.AddToClassList(active_classname);
|
||||
controller.AddToClassList("ndmf-tr");
|
||||
conditionElem.Add(controller);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
var controller = new TextField(active_label);
|
||||
controller.SetEnabled(false);
|
||||
controller.value = condition.DebugReference.ToString();
|
||||
controller.AddToClassList(active_classname);
|
||||
controller.AddToClassList("ndmf-tr");
|
||||
conditionElem.Add(controller);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SetOverallActiveHeader(GameObject obj, Dictionary<TargetProp, object> initialStates)
|
||||
{
|
||||
bool activeState = obj.activeInHierarchy;
|
||||
if (initialStates.TryGetValue(TargetProp.ForObjectActive(obj), out var activeStateObj))
|
||||
{
|
||||
activeState = ((float)activeStateObj) > 0;
|
||||
}
|
||||
var ve_active = e_debugInfo.Q<VisualElement>("state-enabled");
|
||||
var ve_inactive = e_debugInfo.Q<VisualElement>("state-disabled");
|
||||
|
||||
ve_active.style.display = activeState ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
ve_inactive.style.display = activeState ? DisplayStyle.None : DisplayStyle.Flex;
|
||||
}
|
||||
}
|
||||
}
|
3
Editor/ReactiveObjects/Simulator/ROSimulator.cs.meta
Normal file
3
Editor/ReactiveObjects/Simulator/ROSimulator.cs.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7b6dd43c1ba24df49f9254a89b65979a
|
||||
timeCreated: 1724533122
|
193
Editor/ReactiveObjects/Simulator/ROSimulator.uss
Normal file
193
Editor/ReactiveObjects/Simulator/ROSimulator.uss
Normal file
@ -0,0 +1,193 @@
|
||||
.rootVisualContent > TemplateContainer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
LogoImage {
|
||||
padding-bottom: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
#root-box {
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 2px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#content {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#effect-list {
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.effect-group {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.heading {
|
||||
align-self: center;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.h-button-group {
|
||||
flex-direction: row;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.h-button-group StateOverrideController {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.h-button-group Label {
|
||||
flex-grow: 1;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.state-header {
|
||||
font-size: 20px;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.st-enabled {
|
||||
color: green;
|
||||
}
|
||||
|
||||
.st-disabled {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.source-active Label {
|
||||
color: green;
|
||||
}
|
||||
|
||||
.source-inactive Label {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.ag-inactive {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.source-active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.source-inactive {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.h-group {
|
||||
flex-direction: row
|
||||
}
|
||||
|
||||
.h-group > * {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.h-group > Label {
|
||||
flex-grow: 0;
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.h-group > Label.force-center {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.effect-group {
|
||||
border-color: black;
|
||||
border-width: 1px;
|
||||
margin: 2px;
|
||||
padding: 2px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.effect-group .unity-object-field__selector {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.effect-group > * {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.st-active .effect-group {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#effect__source {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#effect-list > Foldout.foldout-open {
|
||||
background-color: rgba(0,0,0,0.1);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
#effect__target {
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
#controlling-conditions {
|
||||
width: 100%;
|
||||
border-top-color: black;
|
||||
border-top-width: 1px;
|
||||
margin-top: 10px;
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
#controlling-conditions Label.heading {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
#controlling-conditions .when-active {
|
||||
display: None;
|
||||
}
|
||||
|
||||
.st-active #controlling-conditions .when-inactive {
|
||||
display: None;
|
||||
}
|
||||
|
||||
.st-active #controlling-conditions .when-active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#controlling-conditions > HelpBox {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
#controlling-conditions > HelpBox .unity-help-box__icon {
|
||||
min-width: 16px;
|
||||
min-height: 16px;
|
||||
}
|
||||
|
||||
.controlling-condition {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.controlling-condition StateOverrideController {
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.controlling-condition ObjectField {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
.controlling-condition ObjectField > Label {
|
||||
-unity-text-align: middle-center;
|
||||
}
|
||||
|
||||
.when-inverted {
|
||||
display: None;
|
||||
}
|
||||
|
||||
.rule-inverted .when-inverted {
|
||||
display: flex;
|
||||
}
|
3
Editor/ReactiveObjects/Simulator/ROSimulator.uss.meta
Normal file
3
Editor/ReactiveObjects/Simulator/ROSimulator.uss.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fa036359c61e4012b59578ee211f73e1
|
||||
timeCreated: 1724533027
|
39
Editor/ReactiveObjects/Simulator/ROSimulator.uxml
Normal file
39
Editor/ReactiveObjects/Simulator/ROSimulator.uxml
Normal file
@ -0,0 +1,39 @@
|
||||
<UXML xmlns:ui="UnityEngine.UIElements" xmlns:ed="UnityEditor.UIElements"
|
||||
xmlns:ma="nadena.dev.modular_avatar.core.editor">
|
||||
<ui:VisualElement name="root-box">
|
||||
<ma:LogoImage/>
|
||||
|
||||
<ui:VisualElement name="content">
|
||||
<ed:ObjectField label="ro_sim.header.inspecting" name="inspecting" class="ndmf-tr"/>
|
||||
<ui:Button text="ro_sim.header.clear_overrides" name="clear-overrides" class="ndmf-tr"/>
|
||||
<ui:VisualElement class="h-divider"/>
|
||||
|
||||
<ui:VisualElement name="debug-info">
|
||||
<ui:Label text="ro_sim.header.object_state" class="heading ndmf-tr"/>
|
||||
<ui:Label text="ro_sim.state.active" name="state-enabled" class="state-header st-enabled ndmf-tr"/>
|
||||
<ui:Label text="ro_sim.state.inactive" name="state-disabled" class="state-header st-disabled ndmf-tr"/>
|
||||
|
||||
<ui:VisualElement class="h-button-group unity-base-field" name="this-obj-override">
|
||||
<ma:StateOverrideController/>
|
||||
<ui:Label text="ro_sim.header.override_gameobject_state" class="unity-base-field__label ndmf-tr"/>
|
||||
</ui:VisualElement>
|
||||
|
||||
<ui:VisualElement class="h-button-group unity-base-field" name="this-menu-override">
|
||||
<ma:StateOverrideController/>
|
||||
<ui:Label text="ro_sim.header.override_menuitem_state" class="unity-base-field__label ndmf-tr"/>
|
||||
</ui:VisualElement>
|
||||
|
||||
<ui:VisualElement class="h-divider"/>
|
||||
<ui:Label text="ro_sim.affected_by.title" class="heading ndmf-tr"/>
|
||||
|
||||
<ui:ScrollView name="effect-list">
|
||||
|
||||
</ui:ScrollView>
|
||||
</ui:VisualElement>
|
||||
</ui:VisualElement>
|
||||
|
||||
<ma:LanguageSwitcherElement/>
|
||||
|
||||
<ui:Button text="debug: reload" name="debug__reload" style="display: none"/>
|
||||
</ui:VisualElement>
|
||||
</UXML>
|
3
Editor/ReactiveObjects/Simulator/ROSimulator.uxml.meta
Normal file
3
Editor/ReactiveObjects/Simulator/ROSimulator.uxml.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 76e8ef6b8e2b44748b921102cb9e7e9a
|
||||
timeCreated: 1724533027
|
52
Editor/ReactiveObjects/Simulator/ROSimulatorButton.cs
Normal file
52
Editor/ReactiveObjects/Simulator/ROSimulatorButton.cs
Normal file
@ -0,0 +1,52 @@
|
||||
using nadena.dev.modular_avatar.core.editor;
|
||||
using nadena.dev.modular_avatar.core.editor.Simulator;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace nadena.dev.modular_avatar.core.editor
|
||||
{
|
||||
internal class ROSimulatorButton : VisualElement
|
||||
{
|
||||
public new class UxmlFactory : UxmlFactory<ROSimulatorButton, UxmlTraits>
|
||||
{
|
||||
}
|
||||
|
||||
public new class UxmlTraits : VisualElement.UxmlTraits
|
||||
{
|
||||
}
|
||||
|
||||
private Button btn;
|
||||
public UnityEngine.Object ReferenceObject;
|
||||
|
||||
public static void BindRefObject(VisualElement elem, UnityEngine.Object obj)
|
||||
{
|
||||
var button = elem.Q<ROSimulatorButton>();
|
||||
|
||||
if (button != null)
|
||||
{
|
||||
button.ReferenceObject = obj;
|
||||
}
|
||||
}
|
||||
|
||||
public ROSimulatorButton()
|
||||
{
|
||||
btn = new Button();
|
||||
btn.AddToClassList("ndmf-tr");
|
||||
btn.text = "ro_sim.open_debugger_button";
|
||||
|
||||
Add(btn);
|
||||
|
||||
btn.clicked += OpenDebugger;
|
||||
}
|
||||
|
||||
private void OpenDebugger()
|
||||
{
|
||||
GameObject target = Selection.activeGameObject;
|
||||
if (ReferenceObject is Component c) target = c.gameObject;
|
||||
else if (ReferenceObject is GameObject go) target = go;
|
||||
|
||||
ROSimulator.OpenDebugger(target);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e63eb3f1209e468cb737b0ec08d8a8c1
|
||||
timeCreated: 1724632949
|
77
Editor/ReactiveObjects/Simulator/StateOverrideController.cs
Normal file
77
Editor/ReactiveObjects/Simulator/StateOverrideController.cs
Normal file
@ -0,0 +1,77 @@
|
||||
using nadena.dev.modular_avatar.core.editor.Simulator;
|
||||
using UnityEditor;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace nadena.dev.modular_avatar.core.editor
|
||||
{
|
||||
internal class StateOverrideController : VisualElement
|
||||
{
|
||||
public new class UxmlFactory : UxmlFactory<StateOverrideController, UxmlTraits>
|
||||
{
|
||||
}
|
||||
|
||||
public new class UxmlTraits : VisualElement.UxmlTraits
|
||||
{
|
||||
}
|
||||
|
||||
private static StyleSheet uss;
|
||||
private Button btn_disable, btn_default, btn_enable;
|
||||
public event System.Action<bool?> OnStateOverrideChanged;
|
||||
|
||||
public StateOverrideController()
|
||||
{
|
||||
uss = AssetDatabase.LoadAssetAtPath<StyleSheet>(ROSimulator.ROOT_PATH + "StateOverrideController.uss");
|
||||
styleSheets.Add(uss);
|
||||
|
||||
AddToClassList("state-override-controller");
|
||||
|
||||
btn_disable = new Button();
|
||||
btn_disable.AddToClassList("btn-disable");
|
||||
btn_default = new Button();
|
||||
btn_default.AddToClassList("btn-default");
|
||||
btn_enable = new Button();
|
||||
btn_enable.AddToClassList("btn-enable");
|
||||
|
||||
btn_disable.text = "-";
|
||||
btn_default.text = " ";
|
||||
btn_enable.text = "+";
|
||||
|
||||
btn_disable.clicked += () => SetStateOverride(false);
|
||||
btn_default.clicked += () => SetStateOverride(null);
|
||||
btn_enable.clicked += () => SetStateOverride(true);
|
||||
|
||||
Add(btn_disable);
|
||||
Add(btn_default);
|
||||
Add(btn_enable);
|
||||
}
|
||||
|
||||
private void SetStateOverride(bool? state)
|
||||
{
|
||||
SetWithoutNotify(state);
|
||||
OnStateOverrideChanged?.Invoke(state);
|
||||
}
|
||||
|
||||
public void SetWithoutNotify(bool? state)
|
||||
{
|
||||
RemoveFromClassList("override-enable");
|
||||
RemoveFromClassList("override-disable");
|
||||
RemoveFromClassList("override-default");
|
||||
|
||||
if (state == null) AddToClassList("override-default");
|
||||
else if (state == true) AddToClassList("override-enable");
|
||||
else AddToClassList("override-disable");
|
||||
}
|
||||
|
||||
public void SetWithoutNotify(ControlCondition condition, float value)
|
||||
{
|
||||
if (value >= condition.ParameterValueLo && value <= condition.ParameterValueHi)
|
||||
{
|
||||
SetWithoutNotify(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
SetWithoutNotify(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4041f4df348b4745ae26140a99d01fd2
|
||||
timeCreated: 1724543447
|
22
Editor/ReactiveObjects/Simulator/StateOverrideController.uss
Normal file
22
Editor/ReactiveObjects/Simulator/StateOverrideController.uss
Normal file
@ -0,0 +1,22 @@
|
||||
.state-override-controller {
|
||||
flex-direction: row;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.state-override-controller > Button {
|
||||
margin: 0;
|
||||
max-width: 16px;
|
||||
min-width: 16px;
|
||||
max-height: 16px;
|
||||
min-height: 16px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.state-override-controller.override-enable .btn-enable {
|
||||
background-color: var(--unity-colors-link-text);
|
||||
}
|
||||
|
||||
.state-override-controller.override-disable .btn-disable {
|
||||
background-color: var(--unity-colors-error-text);
|
||||
color: white;
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0083d985d28a4e698e77e5f2b4d7a680
|
||||
timeCreated: 1724543503
|
@ -20,6 +20,15 @@ namespace nadena.dev.modular_avatar.core
|
||||
private string _cachedPath;
|
||||
private GameObject _cachedReference;
|
||||
|
||||
public AvatarObjectReference Clone()
|
||||
{
|
||||
return new AvatarObjectReference
|
||||
{
|
||||
referencePath = referencePath,
|
||||
targetObject = targetObject
|
||||
};
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
public static GameObject Get(SerializedProperty prop)
|
||||
{
|
||||
|
@ -24,6 +24,17 @@ namespace nadena.dev.modular_avatar.core
|
||||
public ShapeChangeType ChangeType;
|
||||
public float Value;
|
||||
|
||||
public ChangedShape Clone()
|
||||
{
|
||||
return new ChangedShape
|
||||
{
|
||||
Object = Object.Clone(),
|
||||
ShapeName = ShapeName,
|
||||
ChangeType = ChangeType,
|
||||
Value = Value
|
||||
};
|
||||
}
|
||||
|
||||
public bool Equals(ChangedShape other)
|
||||
{
|
||||
return Equals(Object, other.Object) && ShapeName == other.ShapeName && ChangeType == other.ChangeType && Value.Equals(other.Value);
|
||||
|
@ -15,6 +15,9 @@
|
||||
|
||||
internal const string GameObject_EnableInfo = "GameObject/Modular Avatar/Show Modular Avatar Information";
|
||||
internal const int GameObject_EnableInfoOrder = -799;
|
||||
|
||||
internal const string GameObject_ShowReactionDebugger = "GameObject/Modular Avatar/Show Reaction Debugger";
|
||||
internal const int GameObject_ShowReactionDebuggerOrder = GameObject_EnableInfoOrder + 1;
|
||||
|
||||
internal const string GameObject_ExtractMenu = "GameObject/Modular Avatar/Extract Menu";
|
||||
internal const int GameObject_ExtractMenuOrder = GameObject_EnableInfoOrder + 100;
|
||||
|
Binary file not shown.
After Width: | Height: | Size: 30 KiB |
Binary file not shown.
After Width: | Height: | Size: 66 KiB |
41
docs~/docs/reference/reactive-components/debugger/index.md
Normal file
41
docs~/docs/reference/reactive-components/debugger/index.md
Normal file
@ -0,0 +1,41 @@
|
||||
---
|
||||
sidebar_position: 900
|
||||
---
|
||||
|
||||
# Reaction Debugger
|
||||
|
||||
![Reaction Debugger](debugger-main-0.png)
|
||||
|
||||
The Reactive Component Debugger allows you to virtually change the state of menu items and game objects in your scene,
|
||||
so you can test how your reactive components will behave without needing to manually interact with your avatar.
|
||||
|
||||
To open the Reactive Component Debugger, right click a Game Object and choose `Modular Avatar -> Show Reaction Debugger`.
|
||||
Alternatively, you can click the `Open reaction debugger` button on a reactive component.
|
||||
|
||||
The debugger window is divided into two sections: One showing the state of the object itself, and one for the reactions
|
||||
that affect it.
|
||||
|
||||
## Object state section
|
||||
|
||||
![Top section of the debugger](top-section.png)
|
||||
|
||||
The top section lets you select which object you want to examine, by changing the `Inspecting object` field. By default,
|
||||
it will show you the currently selected object, but if you change the field you can examine some other object instead.
|
||||
As with inspector windows, you can lock the view to avoid it changing when you select different objects in the scene.
|
||||
|
||||
Below the `Inspecting object` field, there is a button to clear all active overrides. More on that later. We also see an
|
||||
`Object state: ACTIVE` here, showing that this object is (virtually) active.
|
||||
|
||||
Below that are two options to force the object to be active/inactive, and to force the associated menu item to be selected
|
||||
or deselected. Click the `-` or `+` buttons to trigger such a forced state, or the middle (blank) button to clear the
|
||||
override.
|
||||
|
||||
Forcing an object to be "active" or "inactive" doesn't actually change its state in the hierarchy, but it'll be shown as
|
||||
if it was enabled or disabled in the scene view window.
|
||||
|
||||
## Reaction section
|
||||
|
||||
![Bottom section of the debugger](bottom-section.png)
|
||||
|
||||
The reaction section shows all "incoming" reactions that affect this object. It also shows what conditions need to be
|
||||
true for the reaction to be triggered, and lets you easily override those conditions.
|
Binary file not shown.
After Width: | Height: | Size: 33 KiB |
@ -60,10 +60,9 @@ A -> B -> C, and A is turned off, the timing will be as follows:
|
||||
* Frame 2: A is disabled (B's disable is delayed)
|
||||
* Frame 3: B and C are disabled at the same time.
|
||||
|
||||
### Preview system
|
||||
### Debugging problems
|
||||
|
||||
The effect of reactive components on mesh visibility is immediately reflected in the editor scene view. However, this
|
||||
has some limitations; in particular, it considers the current active state of objects, and the "default" state of Menu
|
||||
Items, but does not consider the effect of Object Toggles on other reactive components. To see the full effect of
|
||||
reactive
|
||||
components, you must enter play mode.
|
||||
The Reactive Objects system includes a debugger which can be used to simulate the effect of toggling on/off various
|
||||
objects or menu items on your avatar. To access it, right click a Game Object and choose
|
||||
`Modular Avatar -> Show Reaction Debugger`. For a detailed description of how it works, see the
|
||||
[debugger documentation](./debugger/index.md).
|
@ -16,6 +16,6 @@
|
||||
},
|
||||
"vpmDependencies": {
|
||||
"com.vrchat.avatars": ">=3.7.0",
|
||||
"nadena.dev.ndmf": ">=1.5.0-rc.0 <2.0.0-a"
|
||||
"nadena.dev.ndmf": ">=1.5.0-rc.1 <2.0.0-a"
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user