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 の完全な効果を確認するには、再生モードに入る必要があります。