diff --git a/Editor/Animation/AnimationServicesContext.cs b/Editor/Animation/AnimationServicesContext.cs
index 3ec0b1ba..e00231e5 100644
--- a/Editor/Animation/AnimationServicesContext.cs
+++ b/Editor/Animation/AnimationServicesContext.cs
@@ -116,8 +116,20 @@ namespace nadena.dev.modular_avatar.animation
return false;
}
- paramName = _readableProperty.ForActiveSelf(_pathMappings.GetObjectIdentifier(obj));
+ paramName = _readableProperty.ForActiveSelf(path);
return true;
}
+
+ public string ForceGetActiveSelfProxy(GameObject obj)
+ {
+ if (_selfProxies.TryGetValue(obj, out var paramName) && !string.IsNullOrEmpty(paramName)) return paramName;
+
+ var path = PathMappings.GetObjectIdentifier(obj);
+
+ paramName = _readableProperty.ForActiveSelf(path);
+ _selfProxies[obj] = paramName;
+
+ return paramName;
+ }
}
}
\ No newline at end of file
diff --git a/Editor/Inspector/ObjectToggle.meta b/Editor/Inspector/ObjectToggle.meta
new file mode 100644
index 00000000..b749d996
--- /dev/null
+++ b/Editor/Inspector/ObjectToggle.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 61045dcdc7f24658a5b47fb0b67ab9fe
+timeCreated: 1722736548
\ No newline at end of file
diff --git a/Editor/Inspector/ObjectToggle/ObjectSwitcher.uxml b/Editor/Inspector/ObjectToggle/ObjectSwitcher.uxml
new file mode 100644
index 00000000..7066a7e0
--- /dev/null
+++ b/Editor/Inspector/ObjectToggle/ObjectSwitcher.uxml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Editor/Inspector/ObjectToggle/ObjectSwitcher.uxml.meta b/Editor/Inspector/ObjectToggle/ObjectSwitcher.uxml.meta
new file mode 100644
index 00000000..dadf2a9a
--- /dev/null
+++ b/Editor/Inspector/ObjectToggle/ObjectSwitcher.uxml.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 02f9cb4b3be34457870f111d73e2fd2f
+timeCreated: 1722736548
\ No newline at end of file
diff --git a/Editor/Inspector/ObjectToggle/ObjectSwitcherEditor.cs b/Editor/Inspector/ObjectToggle/ObjectSwitcherEditor.cs
new file mode 100644
index 00000000..4c231c0d
--- /dev/null
+++ b/Editor/Inspector/ObjectToggle/ObjectSwitcherEditor.cs
@@ -0,0 +1,41 @@
+#region
+
+using System;
+using UnityEditor;
+using UnityEditor.UIElements;
+using UnityEngine;
+using UnityEngine.UIElements;
+
+#endregion
+
+namespace nadena.dev.modular_avatar.core.editor.ShapeChanger
+{
+ [CustomEditor(typeof(ModularAvatarObjectToggle))]
+ public class ObjectSwitcherEditor : MAEditorBase
+ {
+ [SerializeField] private StyleSheet uss;
+ [SerializeField] private VisualTreeAsset uxml;
+
+
+ protected override void OnInnerInspectorGUI()
+ {
+ throw new NotImplementedException();
+ }
+
+ protected override VisualElement CreateInnerInspectorGUI()
+ {
+ var root = uxml.CloneTree();
+ Localization.UI.Localize(root);
+ root.styleSheets.Add(uss);
+
+ root.Bind(serializedObject);
+
+ var listView = root.Q("Shapes");
+
+ listView.showBoundCollectionSize = false;
+ listView.virtualizationMethod = CollectionVirtualizationMethod.DynamicHeight;
+
+ return root;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Editor/Inspector/ObjectToggle/ObjectSwitcherEditor.cs.meta b/Editor/Inspector/ObjectToggle/ObjectSwitcherEditor.cs.meta
new file mode 100644
index 00000000..8847b27b
--- /dev/null
+++ b/Editor/Inspector/ObjectToggle/ObjectSwitcherEditor.cs.meta
@@ -0,0 +1,13 @@
+fileFormatVersion: 2
+guid: a77f3b5f35d04831a7896261cabd3370
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences:
+ - uss: {fileID: 7433441132597879392, guid: b7559b81cea245b68c66602ea0cbbbcf, type: 3}
+ - uxml: {fileID: 9197481963319205126, guid: 02f9cb4b3be34457870f111d73e2fd2f, type: 3}
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Editor/Inspector/ObjectToggle/ObjectSwitcherStyles.uss b/Editor/Inspector/ObjectToggle/ObjectSwitcherStyles.uss
new file mode 100644
index 00000000..db197753
--- /dev/null
+++ b/Editor/Inspector/ObjectToggle/ObjectSwitcherStyles.uss
@@ -0,0 +1,114 @@
+VisualElement {
+}
+
+#group-box {
+ margin-top: 4px;
+ margin-bottom: 4px;
+ padding: 4px;
+ border-width: 3px;
+ border-left-color: rgba(0, 1, 0, 0.2);
+ border-top-color: rgba(0, 1, 0, 0.2);
+ border-right-color: rgba(0, 1, 0, 0.2);
+ border-bottom-color: rgba(0, 1, 0, 0.2);
+ border-radius: 4px;
+ /* background-color: rgba(0, 0, 0, 0.1); */
+}
+
+#ListViewContainer {
+ margin-top: 4px;
+}
+
+#group-box > Label {
+ -unity-font-style: bold;
+}
+
+.group-root {
+ margin-top: 4px;
+}
+
+.group-root Toggle {
+ margin-left: 0;
+}
+
+.group-children {
+ padding-left: 10px;
+}
+
+.left-toggle {
+ display: flex;
+ flex-direction: row;
+}
+
+.toggled-object-editor {
+ flex-direction: row;
+ justify-content: center;
+ align-items: center;
+}
+
+.toggled-object-editor #f-object {
+ flex-grow: 1;
+ height: 100%;
+}
+
+#f-active > Toggle {
+ margin-top: 0;
+ margin-bottom: 0;
+ margin-left: -12px;
+ margin-right: 3px;
+}
+
+.toggled-object-editor PropertyField Label {
+ display: none;
+}
+
+#f-change-type {
+ width: 75px;
+}
+
+.f-value {
+ width: 40px;
+}
+
+#f-value-delete {
+ display: none;
+}
+
+.change-type-delete #f-value {
+ display: none;
+}
+
+.change-type-delete #f-value-delete {
+ display: flex;
+}
+
+/* Add shape window */
+
+.add-shape-popup {
+ margin: 2px;
+}
+
+.vline {
+ width: 100%;
+ height: 4px;
+ border-top-width: 4px;
+ margin-top: 2px;
+ margin-bottom: 2px;
+ border-top-color: rgba(0, 0, 0, 0.2);
+}
+
+.add-shape-row {
+ flex-direction: row;
+}
+
+.add-shape-row Button {
+ flex-grow: 0;
+}
+
+.add-shape-popup ScrollView Label.placeholder {
+ -unity-text-align: middle-center;
+}
+
+.add-shape-row Label {
+ flex-grow: 1;
+ -unity-text-align: middle-left;
+}
diff --git a/Editor/Inspector/ObjectToggle/ObjectSwitcherStyles.uss.meta b/Editor/Inspector/ObjectToggle/ObjectSwitcherStyles.uss.meta
new file mode 100644
index 00000000..9e98834a
--- /dev/null
+++ b/Editor/Inspector/ObjectToggle/ObjectSwitcherStyles.uss.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: b7559b81cea245b68c66602ea0cbbbcf
+timeCreated: 1722736548
\ No newline at end of file
diff --git a/Editor/Inspector/ObjectToggle/ToggledObjectEditor.cs b/Editor/Inspector/ObjectToggle/ToggledObjectEditor.cs
new file mode 100644
index 00000000..62c03c65
--- /dev/null
+++ b/Editor/Inspector/ObjectToggle/ToggledObjectEditor.cs
@@ -0,0 +1,30 @@
+#region
+
+using UnityEditor;
+using UnityEditor.UIElements;
+using UnityEngine.UIElements;
+
+#endregion
+
+namespace nadena.dev.modular_avatar.core.editor.ShapeChanger
+{
+ [CustomPropertyDrawer(typeof(ToggledObject))]
+ public class ToggledObjectEditor : PropertyDrawer
+ {
+ private const string Root = "Packages/nadena.dev.modular-avatar/Editor/Inspector/ObjectSwitcher/";
+ private const string UxmlPath = Root + "ToggledObjectEditor.uxml";
+ private const string UssPath = Root + "ObjectSwitcherStyles.uss";
+
+ public override VisualElement CreatePropertyGUI(SerializedProperty property)
+ {
+ var uxml = AssetDatabase.LoadAssetAtPath(UxmlPath).CloneTree();
+ var uss = AssetDatabase.LoadAssetAtPath(UssPath);
+
+ Localization.UI.Localize(uxml);
+ uxml.styleSheets.Add(uss);
+ uxml.BindProperty(property);
+
+ return uxml;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Editor/Inspector/ObjectToggle/ToggledObjectEditor.cs.meta b/Editor/Inspector/ObjectToggle/ToggledObjectEditor.cs.meta
new file mode 100644
index 00000000..a0e640f3
--- /dev/null
+++ b/Editor/Inspector/ObjectToggle/ToggledObjectEditor.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 0d18528c5f704d3daf1160d9672bd09e
+timeCreated: 1722736548
\ No newline at end of file
diff --git a/Editor/Inspector/ObjectToggle/ToggledObjectEditor.uxml b/Editor/Inspector/ObjectToggle/ToggledObjectEditor.uxml
new file mode 100644
index 00000000..b7f790ba
--- /dev/null
+++ b/Editor/Inspector/ObjectToggle/ToggledObjectEditor.uxml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Editor/Inspector/ObjectToggle/ToggledObjectEditor.uxml.meta b/Editor/Inspector/ObjectToggle/ToggledObjectEditor.uxml.meta
new file mode 100644
index 00000000..dd45cfa8
--- /dev/null
+++ b/Editor/Inspector/ObjectToggle/ToggledObjectEditor.uxml.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 565803cb95a04d1f98f7050c18234cdd
+timeCreated: 1722736548
\ No newline at end of file
diff --git a/Editor/PluginDefinition/PluginDefinition.cs b/Editor/PluginDefinition/PluginDefinition.cs
index 17e5d0e3..775bbf99 100644
--- a/Editor/PluginDefinition/PluginDefinition.cs
+++ b/Editor/PluginDefinition/PluginDefinition.cs
@@ -58,7 +58,7 @@ namespace nadena.dev.modular_avatar.core.editor.plugin
seq.WithRequiredExtension(typeof(AnimationServicesContext), _s2 =>
{
seq.Run("Shape Changer", ctx => new PropertyOverlayPass(ctx).Execute())
- .PreviewingWith(new ShapeChangerPreview());
+ .PreviewingWith(new ShapeChangerPreview(), new ObjectSwitcherPreview());
seq.Run(MergeArmaturePluginPass.Instance);
seq.Run(BoneProxyPluginPass.Instance);
seq.Run(VisibleHeadAccessoryPluginPass.Instance);
diff --git a/Editor/ReactiveObjects.meta b/Editor/ReactiveObjects.meta
new file mode 100644
index 00000000..b9762ec1
--- /dev/null
+++ b/Editor/ReactiveObjects.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 183ea47f9e674c2a8c098b53c98bb38d
+timeCreated: 1722480750
\ No newline at end of file
diff --git a/Editor/ShapeChanger/DESIGN.md b/Editor/ReactiveObjects/DESIGN.md
similarity index 100%
rename from Editor/ShapeChanger/DESIGN.md
rename to Editor/ReactiveObjects/DESIGN.md
diff --git a/Editor/ShapeChanger/DESIGN.md.meta b/Editor/ReactiveObjects/DESIGN.md.meta
similarity index 100%
rename from Editor/ShapeChanger/DESIGN.md.meta
rename to Editor/ReactiveObjects/DESIGN.md.meta
diff --git a/Editor/ReactiveObjects/ObjectTogglePreview.cs b/Editor/ReactiveObjects/ObjectTogglePreview.cs
new file mode 100644
index 00000000..413f17f2
--- /dev/null
+++ b/Editor/ReactiveObjects/ObjectTogglePreview.cs
@@ -0,0 +1,127 @@
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using System.Threading.Tasks;
+using nadena.dev.ndmf.preview;
+using UnityEngine;
+
+namespace nadena.dev.modular_avatar.core.editor
+{
+ internal class ObjectSwitcherPreview : IRenderFilter
+ {
+ public ImmutableList GetTargetGroups(ComputeContext context)
+ {
+ var allToggles = context.GetComponentsByType();
+
+ var objectGroups =
+ new Dictionary.Builder>(
+ new ObjectIdentityComparer());
+
+ foreach (var toggle in allToggles)
+ {
+ context.Observe(toggle,
+ t => t.Objects.Select(o => o.Object.referencePath).ToList(),
+ (x, y) => x.SequenceEqual(y)
+ );
+
+ if (toggle.Objects == null) continue;
+
+ var index = -1;
+ foreach (var switched in toggle.Objects)
+ {
+ index++;
+
+ if (switched.Object == null) continue;
+
+ var target = context.Observe(toggle, _ => switched.Object.Get(toggle));
+
+ if (target == null) continue;
+
+ if (!objectGroups.TryGetValue(target, out var group))
+ {
+ group = ImmutableList.CreateBuilder<(ModularAvatarObjectToggle, int)>();
+ objectGroups[target] = group;
+ }
+
+ group.Add((toggle, index));
+ }
+ }
+
+ var affectedRenderers = objectGroups.Keys
+ .SelectMany(go => context.GetComponentsInChildren(go, true))
+ // If we have overlapping objects, we need to sort by child to parent, so parent configuration overrides
+ // the child. We do this by simply looking at how many times we observe each renderer.
+ .GroupBy(r => r)
+ .Select(g => g.Key)
+ .ToList();
+
+ var renderGroups = new List();
+
+ foreach (var r in affectedRenderers)
+ {
+ var switchers = new List<(ModularAvatarObjectToggle, int)>();
+
+ var obj = r.gameObject;
+ while (obj != null)
+ {
+ var group = objectGroups.GetValueOrDefault(obj);
+ if (group != null) switchers.AddRange(group);
+
+ obj = obj.transform.parent?.gameObject;
+ }
+
+ renderGroups.Add(RenderGroup.For(r).WithData(switchers.ToImmutableList()));
+ }
+
+ return renderGroups.ToImmutableList();
+ }
+
+ public Task Instantiate(RenderGroup group, IEnumerable<(Renderer, Renderer)> proxyPairs,
+ ComputeContext context)
+ {
+ var data = group.GetData>();
+ return new Node(data).Refresh(proxyPairs, context, 0);
+ }
+
+ private class Node : IRenderFilterNode
+ {
+ public RenderAspects WhatChanged => 0;
+
+ private readonly ImmutableList<(ModularAvatarObjectToggle, int)> _controllers;
+
+ public Node(ImmutableList<(ModularAvatarObjectToggle, int)> controllers)
+ {
+ _controllers = controllers;
+ }
+
+ public Task Refresh(IEnumerable<(Renderer, Renderer)> proxyPairs, ComputeContext context,
+ RenderAspects updatedAspects)
+ {
+ foreach (var controller in _controllers)
+ {
+ // Ensure we get awoken whenever there's a change in a controlling component, or its enabled state.
+ context.Observe(controller.Item1);
+ context.ActiveAndEnabled(controller.Item1);
+ }
+
+ return Task.FromResult(this);
+ }
+
+ public void OnFrame(Renderer original, Renderer proxy)
+ {
+ var shouldEnable = true;
+ foreach (var (controller, index) in _controllers)
+ {
+ if (controller == null) continue;
+ if (!controller.gameObject.activeInHierarchy) continue;
+ if (controller.Objects == null || index >= controller.Objects.Count) continue;
+
+ var obj = controller.Objects[index];
+ shouldEnable = obj.Active;
+ }
+
+ proxy.gameObject.SetActive(shouldEnable);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Editor/ReactiveObjects/ObjectTogglePreview.cs.meta b/Editor/ReactiveObjects/ObjectTogglePreview.cs.meta
new file mode 100644
index 00000000..2ea6dd66
--- /dev/null
+++ b/Editor/ReactiveObjects/ObjectTogglePreview.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 9773d1a779874850830b43c4db7c3912
+timeCreated: 1722480946
\ No newline at end of file
diff --git a/Editor/ShapeChanger/PropertyOverlayPass.cs b/Editor/ReactiveObjects/PropertyOverlayPass.cs
similarity index 84%
rename from Editor/ShapeChanger/PropertyOverlayPass.cs
rename to Editor/ReactiveObjects/PropertyOverlayPass.cs
index 6c522a68..9b6e1521 100644
--- a/Editor/ShapeChanger/PropertyOverlayPass.cs
+++ b/Editor/ReactiveObjects/PropertyOverlayPass.cs
@@ -27,7 +27,10 @@ namespace nadena.dev.modular_avatar.core.editor
protected override void Execute(ndmf.BuildContext context)
{
- if (context.AvatarRootObject.GetComponentInChildren() != null)
+ var hasShapeChanger = context.AvatarRootObject.GetComponentInChildren() != null;
+ var hasObjectSwitcher =
+ context.AvatarRootObject.GetComponentInChildren() != null;
+ if (hasShapeChanger || hasObjectSwitcher)
{
var clip = new AnimationClip();
clip.name = "MA Shape Changer Defaults";
@@ -200,6 +203,8 @@ namespace nadena.dev.modular_avatar.core.editor
internal void Execute()
{
Dictionary shapes = FindShapes(context);
+ FindObjectToggles(shapes, context);
+
PreprocessShapes(shapes, out var initialStates, out var deletedShapes);
ProcessInitialStates(initialStates);
@@ -222,6 +227,13 @@ namespace nadena.dev.modular_avatar.core.editor
foreach (var (key, info) in shapes.ToList())
{
+ if (info.actionGroups.Count == 0)
+ {
+ // never active control; ignore it entirely
+ shapes.Remove(key);
+ continue;
+ }
+
var deletions = info.actionGroups.Where(agk => agk.IsDelete).ToList();
if (deletions.Any(d => d.ControllingObject == null))
{
@@ -251,6 +263,10 @@ namespace nadena.dev.modular_avatar.core.editor
private void ProcessInitialStates(Dictionary initialStates)
{
+ // We need to track _two_ initial states: the initial state we'll apply at build time (which applies
+ // when animations are disabled) and the animation base state. Confusingly, the animation base state
+ // should be the state that is currently applied to the object...
+
var clips = context.Extension().AnimationDatabase;
var initialStateHolder = clips.ClipsForPath(PropertyOverlayPrePass.TAG_PATH).FirstOrDefault();
if (initialStateHolder == null) return;
@@ -261,12 +277,12 @@ namespace nadena.dev.modular_avatar.core.editor
foreach (var (key, initialState) in initialStates)
{
- var curve = new AnimationCurve();
- curve.AddKey(0, initialState);
- curve.AddKey(1, initialState);
-
string path;
Type componentType;
+
+ var applied = false;
+ float animBaseState = 0;
+
if (key.TargetObject is GameObject go)
{
path = RuntimeUtil.RelativePath(context.AvatarRootObject, go);
@@ -282,13 +298,42 @@ namespace nadena.dev.modular_avatar.core.editor
var blendShape = key.PropertyName.Substring("blendShape.".Length);
var index = smr.sharedMesh?.GetBlendShapeIndex(blendShape);
- if (index != null && index >= 0) smr.SetBlendShapeWeight(index.Value, initialState);
+ if (index != null && index >= 0)
+ {
+ animBaseState = smr.GetBlendShapeWeight(index.Value);
+ smr.SetBlendShapeWeight(index.Value, initialState);
+ }
+
+ applied = true;
}
}
else
{
throw new InvalidOperationException("Invalid target object: " + key.TargetObject);
}
+
+ if (!applied)
+ {
+ var serializedObject = new SerializedObject(key.TargetObject);
+ var prop = serializedObject.FindProperty(key.PropertyName);
+
+ if (prop != null)
+ switch (prop.propertyType)
+ {
+ case SerializedPropertyType.Boolean:
+ animBaseState = prop.boolValue ? 1 : 0;
+ prop.boolValue = initialState > 0.5f;
+ break;
+ case SerializedPropertyType.Float:
+ animBaseState = prop.floatValue;
+ prop.floatValue = initialState;
+ break;
+ }
+ }
+
+ var curve = new AnimationCurve();
+ curve.AddKey(0, animBaseState);
+ curve.AddKey(1, animBaseState);
var binding = EditorCurveBinding.FloatCurve(
path,
@@ -516,6 +561,16 @@ namespace nadena.dev.modular_avatar.core.editor
var binding = EditorCurveBinding.FloatCurve(path, componentType, key.PropertyName);
AnimationUtility.SetEditorCurve(clip, binding, curve);
+ if (key.TargetObject is GameObject obj && key.PropertyName == "m_IsActive")
+ {
+ var asc = context.Extension();
+ if (asc.TryGetActiveSelfProxy(obj, out var propName))
+ {
+ binding = EditorCurveBinding.FloatCurve("", typeof(Animator), propName);
+ AnimationUtility.SetEditorCurve(clip, binding, curve);
+ }
+ }
+
return clip;
}
@@ -586,6 +641,57 @@ namespace nadena.dev.modular_avatar.core.editor
return clip;
}
+ private void FindObjectToggles(Dictionary objectGroups, ndmf.BuildContext context)
+ {
+ var asc = context.Extension();
+
+ var toggles = this.context.AvatarRootObject.GetComponentsInChildren(true);
+
+ foreach (var toggle in toggles)
+ {
+ if (toggle.Objects == null) continue;
+
+ foreach (var obj in toggle.Objects)
+ {
+ var target = obj.Object.Get(toggle);
+ if (target == null) continue;
+
+ // Make sure we generate an animator prop for each controlled object, as we intend to generate
+ // animations for them.
+ asc.ForceGetActiveSelfProxy(target);
+
+ var key = new TargetProp
+ {
+ TargetObject = target,
+ PropertyName = "m_IsActive"
+ };
+
+ if (!objectGroups.TryGetValue(key, out var group))
+ {
+ group = new PropGroup(key, target.activeSelf ? 1 : 0);
+ objectGroups[key] = group;
+ }
+
+ var value = obj.Active ? 1 : 0;
+ var action = new ActionGroupKey(asc, key, toggle.gameObject, value);
+
+ if (action.ControllingObject == null)
+ {
+ if (action.InitiallyActive)
+ // always active control
+ group.actionGroups.Clear();
+ else
+ // never active control
+ continue;
+ }
+
+ if (group.actionGroups.Count == 0)
+ group.actionGroups.Add(action);
+ else if (!group.actionGroups[^1].TryMerge(action)) group.actionGroups.Add(action);
+ }
+ }
+ }
+
private Dictionary FindShapes(ndmf.BuildContext context)
{
var asc = context.Extension();
diff --git a/Editor/ShapeChanger/PropertyOverlayPass.cs.meta b/Editor/ReactiveObjects/PropertyOverlayPass.cs.meta
similarity index 100%
rename from Editor/ShapeChanger/PropertyOverlayPass.cs.meta
rename to Editor/ReactiveObjects/PropertyOverlayPass.cs.meta
diff --git a/Editor/ShapeChanger/RemoveBlendShapeFromMesh.cs b/Editor/ReactiveObjects/RemoveBlendShapeFromMesh.cs
similarity index 100%
rename from Editor/ShapeChanger/RemoveBlendShapeFromMesh.cs
rename to Editor/ReactiveObjects/RemoveBlendShapeFromMesh.cs
diff --git a/Editor/ShapeChanger/RemoveBlendShapeFromMesh.cs.meta b/Editor/ReactiveObjects/RemoveBlendShapeFromMesh.cs.meta
similarity index 100%
rename from Editor/ShapeChanger/RemoveBlendShapeFromMesh.cs.meta
rename to Editor/ReactiveObjects/RemoveBlendShapeFromMesh.cs.meta
diff --git a/Editor/ShapeChanger/ShapeChangerPreview.cs b/Editor/ReactiveObjects/ShapeChangerPreview.cs
similarity index 100%
rename from Editor/ShapeChanger/ShapeChangerPreview.cs
rename to Editor/ReactiveObjects/ShapeChangerPreview.cs
diff --git a/Editor/ShapeChanger/ShapeChangerPreview.cs.meta b/Editor/ReactiveObjects/ShapeChangerPreview.cs.meta
similarity index 100%
rename from Editor/ShapeChanger/ShapeChangerPreview.cs.meta
rename to Editor/ReactiveObjects/ShapeChangerPreview.cs.meta
diff --git a/Editor/ShapeChanger.meta b/Editor/ShapeChanger.meta
deleted file mode 100644
index 5a38a710..00000000
--- a/Editor/ShapeChanger.meta
+++ /dev/null
@@ -1,3 +0,0 @@
-fileFormatVersion: 2
-guid: 57bb6d1a371c4993bf7cfed797b2eb65
-timeCreated: 1717105136
\ No newline at end of file
diff --git a/Runtime/ModularAvatarObjectToggle.cs b/Runtime/ModularAvatarObjectToggle.cs
new file mode 100644
index 00000000..532add7b
--- /dev/null
+++ b/Runtime/ModularAvatarObjectToggle.cs
@@ -0,0 +1,26 @@
+using System;
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace nadena.dev.modular_avatar.core
+{
+ [Serializable]
+ public struct ToggledObject
+ {
+ public AvatarObjectReference Object;
+ public bool Active;
+ }
+
+ [AddComponentMenu("Modular Avatar/MA Object Toggle")]
+ [HelpURL("https://modular-avatar.nadena.dev/docs/reference/object-toggle?lang=auto")]
+ public class ModularAvatarObjectToggle : AvatarTagComponent
+ {
+ [SerializeField] private List m_objects = new();
+
+ public List Objects
+ {
+ get => m_objects;
+ set => m_objects = value;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Runtime/ModularAvatarObjectToggle.cs.meta b/Runtime/ModularAvatarObjectToggle.cs.meta
new file mode 100644
index 00000000..867d88ee
--- /dev/null
+++ b/Runtime/ModularAvatarObjectToggle.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: a162bb8ec7e24a5abcf457887f1df3fa
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {fileID: 2800000, guid: a8edd5bd1a0a64a40aa99cc09fb5f198, type: 3}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/UnitTests~/ShapeChanger/InitialStates/SCDefaultAnimation.cs b/UnitTests~/ShapeChanger/InitialStates/SCDefaultAnimation.cs
index 8248d00b..d1ee2ff4 100644
--- a/UnitTests~/ShapeChanger/InitialStates/SCDefaultAnimation.cs
+++ b/UnitTests~/ShapeChanger/InitialStates/SCDefaultAnimation.cs
@@ -26,6 +26,9 @@ namespace ShapeChangerTests
Assert.NotNull(subBt);
var clip = subBt.children[0].motion as AnimationClip;
Assert.NotNull(clip);
+
+ var smr = root.transform.Find("test mesh").GetComponent();
+ var sharedMesh = smr.sharedMesh;
var bindings = AnimationUtility.GetCurveBindings(clip);
var curve = AnimationUtility.GetEditorCurve(clip, EditorCurveBinding.FloatCurve(
@@ -33,8 +36,8 @@ namespace ShapeChangerTests
typeof(SkinnedMeshRenderer),
"blendShape.key1"
));
- Assert.AreEqual(10.0f, curve.keys[0].value, 0.1f);
- Assert.AreEqual(10.0f, curve.keys[1].value, 0.1f);
+ Assert.AreEqual(7.0f, curve.keys[0].value, 0.1f);
+ Assert.AreEqual(7.0f, curve.keys[1].value, 0.1f);
curve = AnimationUtility.GetEditorCurve(clip, EditorCurveBinding.FloatCurve(
"test mesh",
@@ -49,14 +52,12 @@ namespace ShapeChangerTests
typeof(SkinnedMeshRenderer),
"blendShape.key3"
));
- Assert.AreEqual(100.0f, curve.keys[0].value, 0.1f);
- Assert.AreEqual(100.0f, curve.keys[1].value, 0.1f);
+ Assert.AreEqual(6.0f, curve.keys[0].value, 0.1f);
+ Assert.AreEqual(6.0f, curve.keys[1].value, 0.1f);
// Check actual blendshape states
- var smr = root.transform.Find("test mesh").GetComponent();
- var sharedMesh = smr.sharedMesh;
Assert.AreEqual(10.0f, smr.GetBlendShapeWeight(sharedMesh.GetBlendShapeIndex("key1")), 0.1f);
- Assert.AreEqual(0.0f, smr.GetBlendShapeWeight(sharedMesh.GetBlendShapeIndex("key2")), 0.1f);
+ Assert.AreEqual(5.0f, smr.GetBlendShapeWeight(sharedMesh.GetBlendShapeIndex("key2")), 0.1f);
Assert.AreEqual(100.0f, smr.GetBlendShapeWeight(sharedMesh.GetBlendShapeIndex("key3")), 0.1f);
}
}
diff --git a/UnitTests~/ShapeChanger/InitialStates/SCDefaultAnimation.prefab b/UnitTests~/ShapeChanger/InitialStates/SCDefaultAnimation.prefab
index 6e9f300d..d7bed984 100644
--- a/UnitTests~/ShapeChanger/InitialStates/SCDefaultAnimation.prefab
+++ b/UnitTests~/ShapeChanger/InitialStates/SCDefaultAnimation.prefab
@@ -600,6 +600,21 @@ PrefabInstance:
propertyPath: m_AABB.m_Extent.y
value: 1
objectReference: {fileID: 0}
+ - target: {fileID: -3887185075125053422, guid: cd28f61dacdc2424d951194ff69ba154,
+ type: 3}
+ propertyPath: m_BlendShapeWeights.Array.data[0]
+ value: 5
+ objectReference: {fileID: 0}
+ - target: {fileID: -3887185075125053422, guid: cd28f61dacdc2424d951194ff69ba154,
+ type: 3}
+ propertyPath: m_BlendShapeWeights.Array.data[1]
+ value: 6
+ objectReference: {fileID: 0}
+ - target: {fileID: -3887185075125053422, guid: cd28f61dacdc2424d951194ff69ba154,
+ type: 3}
+ propertyPath: m_BlendShapeWeights.Array.data[2]
+ value: 7
+ objectReference: {fileID: 0}
- target: {fileID: 919132149155446097, guid: cd28f61dacdc2424d951194ff69ba154,
type: 3}
propertyPath: m_Name
diff --git a/docs~/docs/reference/object-toggle.md b/docs~/docs/reference/object-toggle.md
new file mode 100644
index 00000000..6c78ffb5
--- /dev/null
+++ b/docs~/docs/reference/object-toggle.md
@@ -0,0 +1,43 @@
+# Object Toggle
+
+
+
+The Object Toggle component allows you to change the active state of one or more other GameObjects, based on the active
+state of a controlling object.
+
+## When should I use it?
+
+This component is useful to disable one mesh when another mesh is covering it entirely. For example, you might want to
+disable an underwear mesh when it's fully covered by other clothing.
+
+## Setting up Object Toggle
+
+Simply add an Object Toggle component to the controlling object, then click the + and select a target object to be
+controlled. The checkmark controls whether the target object will be enabled or disabled.
+
+### Conflict resolution
+
+When multiple Object Toggles are active and try to control the same target object, the Object Toggle that is last in
+hierarchy order will win. When no Object Toggles are active, the original state of the object, or animated state (if
+some other animation is trying to animate that object) wins.
+
+### Response timing
+
+Object Toggle updates the affected objects one frame after the controlling object is updated. To avoid any unfortunate
+"accidents", when an Object Toggle is disabled, the object that was disabled (either the Object Toggle itself or one of
+its parents) in its parent hierarchy will be disabled one frame later than they would otherwise. This ensures that if
+you use Object Toggle to hide a mesh when it's fully covered, the covering mesh will remain visible until the same frame
+as when the inner mesh is enabled again.
+
+When you use an Object Toggle to control another Object Toggle, this delay only applies to each Object Toggle
+individually. That is, if you have A -> B -> C, and A is turned off, the timing will be as follows:
+
+* Frame 1: Nothing happens (A's disable is delayed)
+* Frame 2: A is disabled (B's disable is delayed)
+* Frame 3: B and C are disabled at the same time.
+
+### Preview system limitations
+
+The effect of Object Toggles on mesh visibility is immediately reflected in the editor scene view. However, the impact
+of Object Toggles on other responsive components, such as other Object Toggles or [Shape Changers](./shape-changer.md)
+will not be reflected in the preview display. To see the full effect of Object Toggles, you must enter play mode.
\ No newline at end of file
diff --git a/docs~/i18n/ja/docusaurus-plugin-content-docs/current/reference/object-toggle.md b/docs~/i18n/ja/docusaurus-plugin-content-docs/current/reference/object-toggle.md
new file mode 100644
index 00000000..f3f34e85
--- /dev/null
+++ b/docs~/i18n/ja/docusaurus-plugin-content-docs/current/reference/object-toggle.md
@@ -0,0 +1,44 @@
+# Object Toggle
+
+
+
+Object Toggle コンポーネントは、制御オブジェクトのアクティブ状態に基づいて、他のいくつかのGameObjectのアクティブ状態を変更するコンポーネントです。
+
+## いつ使うべきか?
+
+このコンポーネントは、他の衣服に完全に覆われたときに、隠されたメッシュを無効にしたい場合に便利です。例えば、他の衣服に完全に覆われたときに
+下着メッシュを無効にしたい場合に使えます。
+
+## Object Toggleの設定
+
+制御オブジェクトに Object Toggle コンポーネントを追加し、+ をクリックして制御される対象オブジェクトを選択します。
+チェックマークは、対象オブジェクトが有効か無効かを制御します。
+
+### コンフリクト解決
+
+複数の Object Toggle がアクティブで、同じ対象オブジェクトを制御しようとする場合、階層順に最後になる Object Toggle
+の設定が採用されます。
+
+対象オブジェクトを操作する Object Toggle がすべて非アクティブ状態の場合、オブジェクトの元の状態、または(他のアニメーションがそのオブジェクト
+を操作している場合)アニメーション状態が採用されます。
+
+### 応答タイミング
+
+Object Toggle は、制御オブジェクトが更新された後の1フレーム後に影響を受けるオブジェクトを更新します。不幸な「事故」を避けるため、
+Object Toggle が無効になると、無効になったオブジェクト(Object Toggle 自体か、その親など)が本来より1フレーム後に無効になります。
+これにより、外側の衣装を無効化するとき、内側のメッシュが再度有効になるまで、外側のメッシュが表示され続けることが保証されます。
+
+Object Toggle を使用して他の Object Toggle を制御する場合、この遅延は各 Object Toggle にのみ適用されます。つまり、A -> B ->
+C という
+構造で A がオフになった場合、タイミングは次のようになります:
+
+* フレーム1:何も起こらない(Aの無効化が遅延)
+* フレーム2:Aが無効化される(Bの無効化が遅延)
+* フレーム3:BとCが同時に無効化されます。
+
+### プレビューシステムの制限
+
+Object Toggle がメッシュの可視性に与える影響は、エディタのシーンビューにすぐに反映されます。ただし、Object Toggle
+が他の応答コンポーネント、
+例えば他の Object Toggle または [Shape Changers](./shape-changer.md) に与える影響は、プレビュー表示には反映されません。
+Object Toggle の完全な効果を確認するには、再生モードに入る必要があります。