mirror of
https://github.com/bdunderscore/modular-avatar.git
synced 2025-02-12 00:32:47 +08:00
feat: add Object Toggle component (#942)
* feat: add Object Toggle component * docs: Object Toggle docs * chore: add missing HelpURL * chore: fix broken test
This commit is contained in:
parent
366ff0832f
commit
32dc864d8d
@ -116,8 +116,20 @@ namespace nadena.dev.modular_avatar.animation
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
paramName = _readableProperty.ForActiveSelf(_pathMappings.GetObjectIdentifier(obj));
|
paramName = _readableProperty.ForActiveSelf(path);
|
||||||
return true;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
3
Editor/Inspector/ObjectToggle.meta
Normal file
3
Editor/Inspector/ObjectToggle.meta
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 61045dcdc7f24658a5b47fb0b67ab9fe
|
||||||
|
timeCreated: 1722736548
|
22
Editor/Inspector/ObjectToggle/ObjectSwitcher.uxml
Normal file
22
Editor/Inspector/ObjectToggle/ObjectSwitcher.uxml
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<UXML xmlns:ui="UnityEngine.UIElements"
|
||||||
|
xmlns:ma="nadena.dev.modular_avatar.core.editor">
|
||||||
|
<ui:VisualElement name="root-box">
|
||||||
|
<ui:VisualElement name="group-box">
|
||||||
|
<ui:VisualElement name="ListViewContainer">
|
||||||
|
<ui:ListView virtualization-method="DynamicHeight"
|
||||||
|
reorder-mode="Animated"
|
||||||
|
reorderable="true"
|
||||||
|
show-add-remove-footer="true"
|
||||||
|
show-border="true"
|
||||||
|
show-foldout-header="false"
|
||||||
|
name="Shapes"
|
||||||
|
item-height="100"
|
||||||
|
binding-path="m_objects"
|
||||||
|
style="flex-grow: 1;"
|
||||||
|
/>
|
||||||
|
</ui:VisualElement>
|
||||||
|
</ui:VisualElement>
|
||||||
|
|
||||||
|
<ma:LanguageSwitcherElement/>
|
||||||
|
</ui:VisualElement>
|
||||||
|
</UXML>
|
3
Editor/Inspector/ObjectToggle/ObjectSwitcher.uxml.meta
Normal file
3
Editor/Inspector/ObjectToggle/ObjectSwitcher.uxml.meta
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 02f9cb4b3be34457870f111d73e2fd2f
|
||||||
|
timeCreated: 1722736548
|
41
Editor/Inspector/ObjectToggle/ObjectSwitcherEditor.cs
Normal file
41
Editor/Inspector/ObjectToggle/ObjectSwitcherEditor.cs
Normal file
@ -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<ListView>("Shapes");
|
||||||
|
|
||||||
|
listView.showBoundCollectionSize = false;
|
||||||
|
listView.virtualizationMethod = CollectionVirtualizationMethod.DynamicHeight;
|
||||||
|
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
13
Editor/Inspector/ObjectToggle/ObjectSwitcherEditor.cs.meta
Normal file
13
Editor/Inspector/ObjectToggle/ObjectSwitcherEditor.cs.meta
Normal file
@ -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:
|
114
Editor/Inspector/ObjectToggle/ObjectSwitcherStyles.uss
Normal file
114
Editor/Inspector/ObjectToggle/ObjectSwitcherStyles.uss
Normal file
@ -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;
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: b7559b81cea245b68c66602ea0cbbbcf
|
||||||
|
timeCreated: 1722736548
|
30
Editor/Inspector/ObjectToggle/ToggledObjectEditor.cs
Normal file
30
Editor/Inspector/ObjectToggle/ToggledObjectEditor.cs
Normal file
@ -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<VisualTreeAsset>(UxmlPath).CloneTree();
|
||||||
|
var uss = AssetDatabase.LoadAssetAtPath<StyleSheet>(UssPath);
|
||||||
|
|
||||||
|
Localization.UI.Localize(uxml);
|
||||||
|
uxml.styleSheets.Add(uss);
|
||||||
|
uxml.BindProperty(property);
|
||||||
|
|
||||||
|
return uxml;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 0d18528c5f704d3daf1160d9672bd09e
|
||||||
|
timeCreated: 1722736548
|
6
Editor/Inspector/ObjectToggle/ToggledObjectEditor.uxml
Normal file
6
Editor/Inspector/ObjectToggle/ToggledObjectEditor.uxml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<UXML xmlns:ui="UnityEngine.UIElements" xmlns:ed="UnityEditor.UIElements">
|
||||||
|
<ui:VisualElement class="toggled-object-editor">
|
||||||
|
<ed:PropertyField binding-path="Active" label="" name="f-active"/>
|
||||||
|
<ed:PropertyField binding-path="Object" label="" name="f-object" class="f-object"/>
|
||||||
|
</ui:VisualElement>
|
||||||
|
</UXML>
|
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 565803cb95a04d1f98f7050c18234cdd
|
||||||
|
timeCreated: 1722736548
|
@ -58,7 +58,7 @@ namespace nadena.dev.modular_avatar.core.editor.plugin
|
|||||||
seq.WithRequiredExtension(typeof(AnimationServicesContext), _s2 =>
|
seq.WithRequiredExtension(typeof(AnimationServicesContext), _s2 =>
|
||||||
{
|
{
|
||||||
seq.Run("Shape Changer", ctx => new PropertyOverlayPass(ctx).Execute())
|
seq.Run("Shape Changer", ctx => new PropertyOverlayPass(ctx).Execute())
|
||||||
.PreviewingWith(new ShapeChangerPreview());
|
.PreviewingWith(new ShapeChangerPreview(), new ObjectSwitcherPreview());
|
||||||
seq.Run(MergeArmaturePluginPass.Instance);
|
seq.Run(MergeArmaturePluginPass.Instance);
|
||||||
seq.Run(BoneProxyPluginPass.Instance);
|
seq.Run(BoneProxyPluginPass.Instance);
|
||||||
seq.Run(VisibleHeadAccessoryPluginPass.Instance);
|
seq.Run(VisibleHeadAccessoryPluginPass.Instance);
|
||||||
|
3
Editor/ReactiveObjects.meta
Normal file
3
Editor/ReactiveObjects.meta
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 183ea47f9e674c2a8c098b53c98bb38d
|
||||||
|
timeCreated: 1722480750
|
127
Editor/ReactiveObjects/ObjectTogglePreview.cs
Normal file
127
Editor/ReactiveObjects/ObjectTogglePreview.cs
Normal file
@ -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<RenderGroup> GetTargetGroups(ComputeContext context)
|
||||||
|
{
|
||||||
|
var allToggles = context.GetComponentsByType<ModularAvatarObjectToggle>();
|
||||||
|
|
||||||
|
var objectGroups =
|
||||||
|
new Dictionary<GameObject, ImmutableList<(ModularAvatarObjectToggle, int)>.Builder>(
|
||||||
|
new ObjectIdentityComparer<GameObject>());
|
||||||
|
|
||||||
|
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<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)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var renderGroups = new List<RenderGroup>();
|
||||||
|
|
||||||
|
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<IRenderFilterNode> Instantiate(RenderGroup group, IEnumerable<(Renderer, Renderer)> proxyPairs,
|
||||||
|
ComputeContext context)
|
||||||
|
{
|
||||||
|
var data = group.GetData<ImmutableList<(ModularAvatarObjectToggle, int)>>();
|
||||||
|
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<IRenderFilterNode> 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<IRenderFilterNode>(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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
3
Editor/ReactiveObjects/ObjectTogglePreview.cs.meta
Normal file
3
Editor/ReactiveObjects/ObjectTogglePreview.cs.meta
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 9773d1a779874850830b43c4db7c3912
|
||||||
|
timeCreated: 1722480946
|
@ -27,7 +27,10 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
|
|
||||||
protected override void Execute(ndmf.BuildContext context)
|
protected override void Execute(ndmf.BuildContext context)
|
||||||
{
|
{
|
||||||
if (context.AvatarRootObject.GetComponentInChildren<ModularAvatarShapeChanger>() != null)
|
var hasShapeChanger = context.AvatarRootObject.GetComponentInChildren<ModularAvatarShapeChanger>() != null;
|
||||||
|
var hasObjectSwitcher =
|
||||||
|
context.AvatarRootObject.GetComponentInChildren<ModularAvatarObjectToggle>() != null;
|
||||||
|
if (hasShapeChanger || hasObjectSwitcher)
|
||||||
{
|
{
|
||||||
var clip = new AnimationClip();
|
var clip = new AnimationClip();
|
||||||
clip.name = "MA Shape Changer Defaults";
|
clip.name = "MA Shape Changer Defaults";
|
||||||
@ -200,6 +203,8 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
internal void Execute()
|
internal void Execute()
|
||||||
{
|
{
|
||||||
Dictionary<TargetProp, PropGroup> shapes = FindShapes(context);
|
Dictionary<TargetProp, PropGroup> shapes = FindShapes(context);
|
||||||
|
FindObjectToggles(shapes, context);
|
||||||
|
|
||||||
PreprocessShapes(shapes, out var initialStates, out var deletedShapes);
|
PreprocessShapes(shapes, out var initialStates, out var deletedShapes);
|
||||||
|
|
||||||
ProcessInitialStates(initialStates);
|
ProcessInitialStates(initialStates);
|
||||||
@ -222,6 +227,13 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
|
|
||||||
foreach (var (key, info) in shapes.ToList())
|
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();
|
var deletions = info.actionGroups.Where(agk => agk.IsDelete).ToList();
|
||||||
if (deletions.Any(d => d.ControllingObject == null))
|
if (deletions.Any(d => d.ControllingObject == null))
|
||||||
{
|
{
|
||||||
@ -251,6 +263,10 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
|
|
||||||
private void ProcessInitialStates(Dictionary<TargetProp, float> initialStates)
|
private void ProcessInitialStates(Dictionary<TargetProp, float> 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<AnimationServicesContext>().AnimationDatabase;
|
var clips = context.Extension<AnimationServicesContext>().AnimationDatabase;
|
||||||
var initialStateHolder = clips.ClipsForPath(PropertyOverlayPrePass.TAG_PATH).FirstOrDefault();
|
var initialStateHolder = clips.ClipsForPath(PropertyOverlayPrePass.TAG_PATH).FirstOrDefault();
|
||||||
if (initialStateHolder == null) return;
|
if (initialStateHolder == null) return;
|
||||||
@ -261,12 +277,12 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
|
|
||||||
foreach (var (key, initialState) in initialStates)
|
foreach (var (key, initialState) in initialStates)
|
||||||
{
|
{
|
||||||
var curve = new AnimationCurve();
|
|
||||||
curve.AddKey(0, initialState);
|
|
||||||
curve.AddKey(1, initialState);
|
|
||||||
|
|
||||||
string path;
|
string path;
|
||||||
Type componentType;
|
Type componentType;
|
||||||
|
|
||||||
|
var applied = false;
|
||||||
|
float animBaseState = 0;
|
||||||
|
|
||||||
if (key.TargetObject is GameObject go)
|
if (key.TargetObject is GameObject go)
|
||||||
{
|
{
|
||||||
path = RuntimeUtil.RelativePath(context.AvatarRootObject, go);
|
path = RuntimeUtil.RelativePath(context.AvatarRootObject, go);
|
||||||
@ -282,7 +298,13 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
var blendShape = key.PropertyName.Substring("blendShape.".Length);
|
var blendShape = key.PropertyName.Substring("blendShape.".Length);
|
||||||
var index = smr.sharedMesh?.GetBlendShapeIndex(blendShape);
|
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
|
else
|
||||||
@ -290,6 +312,29 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
throw new InvalidOperationException("Invalid target object: " + key.TargetObject);
|
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(
|
var binding = EditorCurveBinding.FloatCurve(
|
||||||
path,
|
path,
|
||||||
componentType,
|
componentType,
|
||||||
@ -516,6 +561,16 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
var binding = EditorCurveBinding.FloatCurve(path, componentType, key.PropertyName);
|
var binding = EditorCurveBinding.FloatCurve(path, componentType, key.PropertyName);
|
||||||
AnimationUtility.SetEditorCurve(clip, binding, curve);
|
AnimationUtility.SetEditorCurve(clip, binding, curve);
|
||||||
|
|
||||||
|
if (key.TargetObject is GameObject obj && key.PropertyName == "m_IsActive")
|
||||||
|
{
|
||||||
|
var asc = context.Extension<AnimationServicesContext>();
|
||||||
|
if (asc.TryGetActiveSelfProxy(obj, out var propName))
|
||||||
|
{
|
||||||
|
binding = EditorCurveBinding.FloatCurve("", typeof(Animator), propName);
|
||||||
|
AnimationUtility.SetEditorCurve(clip, binding, curve);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return clip;
|
return clip;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -586,6 +641,57 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
return clip;
|
return clip;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void FindObjectToggles(Dictionary<TargetProp, PropGroup> objectGroups, ndmf.BuildContext context)
|
||||||
|
{
|
||||||
|
var asc = context.Extension<AnimationServicesContext>();
|
||||||
|
|
||||||
|
var toggles = this.context.AvatarRootObject.GetComponentsInChildren<ModularAvatarObjectToggle>(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<TargetProp, PropGroup> FindShapes(ndmf.BuildContext context)
|
private Dictionary<TargetProp, PropGroup> FindShapes(ndmf.BuildContext context)
|
||||||
{
|
{
|
||||||
var asc = context.Extension<AnimationServicesContext>();
|
var asc = context.Extension<AnimationServicesContext>();
|
@ -1,3 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 57bb6d1a371c4993bf7cfed797b2eb65
|
|
||||||
timeCreated: 1717105136
|
|
26
Runtime/ModularAvatarObjectToggle.cs
Normal file
26
Runtime/ModularAvatarObjectToggle.cs
Normal file
@ -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<ToggledObject> m_objects = new();
|
||||||
|
|
||||||
|
public List<ToggledObject> Objects
|
||||||
|
{
|
||||||
|
get => m_objects;
|
||||||
|
set => m_objects = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
11
Runtime/ModularAvatarObjectToggle.cs.meta
Normal file
11
Runtime/ModularAvatarObjectToggle.cs.meta
Normal file
@ -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:
|
@ -27,14 +27,17 @@ namespace ShapeChangerTests
|
|||||||
var clip = subBt.children[0].motion as AnimationClip;
|
var clip = subBt.children[0].motion as AnimationClip;
|
||||||
Assert.NotNull(clip);
|
Assert.NotNull(clip);
|
||||||
|
|
||||||
|
var smr = root.transform.Find("test mesh").GetComponent<SkinnedMeshRenderer>();
|
||||||
|
var sharedMesh = smr.sharedMesh;
|
||||||
|
|
||||||
var bindings = AnimationUtility.GetCurveBindings(clip);
|
var bindings = AnimationUtility.GetCurveBindings(clip);
|
||||||
var curve = AnimationUtility.GetEditorCurve(clip, EditorCurveBinding.FloatCurve(
|
var curve = AnimationUtility.GetEditorCurve(clip, EditorCurveBinding.FloatCurve(
|
||||||
"test mesh",
|
"test mesh",
|
||||||
typeof(SkinnedMeshRenderer),
|
typeof(SkinnedMeshRenderer),
|
||||||
"blendShape.key1"
|
"blendShape.key1"
|
||||||
));
|
));
|
||||||
Assert.AreEqual(10.0f, curve.keys[0].value, 0.1f);
|
Assert.AreEqual(7.0f, curve.keys[0].value, 0.1f);
|
||||||
Assert.AreEqual(10.0f, curve.keys[1].value, 0.1f);
|
Assert.AreEqual(7.0f, curve.keys[1].value, 0.1f);
|
||||||
|
|
||||||
curve = AnimationUtility.GetEditorCurve(clip, EditorCurveBinding.FloatCurve(
|
curve = AnimationUtility.GetEditorCurve(clip, EditorCurveBinding.FloatCurve(
|
||||||
"test mesh",
|
"test mesh",
|
||||||
@ -49,14 +52,12 @@ namespace ShapeChangerTests
|
|||||||
typeof(SkinnedMeshRenderer),
|
typeof(SkinnedMeshRenderer),
|
||||||
"blendShape.key3"
|
"blendShape.key3"
|
||||||
));
|
));
|
||||||
Assert.AreEqual(100.0f, curve.keys[0].value, 0.1f);
|
Assert.AreEqual(6.0f, curve.keys[0].value, 0.1f);
|
||||||
Assert.AreEqual(100.0f, curve.keys[1].value, 0.1f);
|
Assert.AreEqual(6.0f, curve.keys[1].value, 0.1f);
|
||||||
|
|
||||||
// Check actual blendshape states
|
// Check actual blendshape states
|
||||||
var smr = root.transform.Find("test mesh").GetComponent<SkinnedMeshRenderer>();
|
|
||||||
var sharedMesh = smr.sharedMesh;
|
|
||||||
Assert.AreEqual(10.0f, smr.GetBlendShapeWeight(sharedMesh.GetBlendShapeIndex("key1")), 0.1f);
|
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);
|
Assert.AreEqual(100.0f, smr.GetBlendShapeWeight(sharedMesh.GetBlendShapeIndex("key3")), 0.1f);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -600,6 +600,21 @@ PrefabInstance:
|
|||||||
propertyPath: m_AABB.m_Extent.y
|
propertyPath: m_AABB.m_Extent.y
|
||||||
value: 1
|
value: 1
|
||||||
objectReference: {fileID: 0}
|
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,
|
- target: {fileID: 919132149155446097, guid: cd28f61dacdc2424d951194ff69ba154,
|
||||||
type: 3}
|
type: 3}
|
||||||
propertyPath: m_Name
|
propertyPath: m_Name
|
||||||
|
43
docs~/docs/reference/object-toggle.md
Normal file
43
docs~/docs/reference/object-toggle.md
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
# Object Toggle
|
||||||
|
|
||||||
|
<!-- TODO: Screenshot -->
|
||||||
|
|
||||||
|
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.
|
@ -0,0 +1,44 @@
|
|||||||
|
# Object Toggle
|
||||||
|
|
||||||
|
<!-- TODO: Screenshot -->
|
||||||
|
|
||||||
|
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 の完全な効果を確認するには、再生モードに入る必要があります。
|
Loading…
Reference in New Issue
Block a user