mirror of
https://github.com/bdunderscore/modular-avatar.git
synced 2025-01-31 02:32:53 +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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
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.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);
|
||||
|
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)
|
||||
{
|
||||
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();
|
||||
clip.name = "MA Shape Changer Defaults";
|
||||
@ -200,6 +203,8 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
internal void Execute()
|
||||
{
|
||||
Dictionary<TargetProp, PropGroup> 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<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 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<AnimationServicesContext>();
|
||||
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<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)
|
||||
{
|
||||
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:
|
@ -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<SkinnedMeshRenderer>();
|
||||
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<SkinnedMeshRenderer>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
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