Merge branch 'main' into menu-richtext

This commit is contained in:
bd_ 2024-12-01 15:41:30 -08:00 committed by GitHub
commit 6dcd63dde7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
167 changed files with 8333 additions and 2803 deletions

View File

@ -1,25 +1,25 @@
{
"dependencies": {
"com.vrchat.avatars": {
"version": "3.7.0"
"version": "3.7.4"
},
"nadena.dev.ndmf": {
"version": "1.4.0"
"version": "1.6.0"
}
},
"locked": {
"com.vrchat.avatars": {
"version": "3.7.0",
"version": "3.7.4",
"dependencies": {
"com.vrchat.base": "3.7.0"
"com.vrchat.base": "3.7.4"
}
},
"com.vrchat.base": {
"version": "3.7.0",
"version": "3.7.4",
"dependencies": {}
},
"nadena.dev.ndmf": {
"version": "1.5.0"
"version": "1.6.0"
}
}
}

View File

@ -79,7 +79,7 @@ jobs:
path: ${{ env.zipFile }}
- name: Make Release
uses: softprops/action-gh-release@c062e08bd532815e2082a85e87e3ef29c3e6d191
uses: softprops/action-gh-release@01570a1f39cb168c169c802c3bceb9e93fb10974
if: startsWith(github.ref, 'refs/tags/')
with:
draft: true

View File

@ -122,7 +122,7 @@ jobs:
workingDirectory: docs-site~
- name: Purge cache
uses: nathanvaughn/actions-cloudflare-purge@cd4afdf666c2e6a6720048f27ac9cbdd664a673a
uses: nathanvaughn/actions-cloudflare-purge@992cc4e96422fb8ddf077281678373fe41e7736c
continue-on-error: true
with:
cf_zone: ${{ secrets.CF_ZONE_ID }}

View File

@ -116,6 +116,7 @@ jobs:
with:
repos: |
https://vpm.nadena.dev/vpm-prerelease.json
https://vrchat.github.io/packages/index.json?download
- if: ${{ steps.setup.outputs.should_test == 'true' }}
name: "Debug: List project contents"

View File

@ -53,7 +53,21 @@ namespace nadena.dev.modular_avatar.animation
set
{
_originalClip = value;
IsProxyAnimation = value != null && Util.IsProxyAnimation(value);
var baseClip = ObjectRegistry.GetReference(value)?.Object as AnimationClip;
IsProxyAnimation = false;
if (value != null && Util.IsProxyAnimation(value))
{
IsProxyAnimation = true;
}
else if (baseClip != null && Util.IsProxyAnimation(baseClip))
{
// RenameParametersPass replaces proxy clips outside of the purview of the animation database,
// so trace this using ObjectRegistry and correct the reference.
IsProxyAnimation = true;
_originalClip = baseClip;
}
}
}
@ -144,6 +158,7 @@ namespace nadena.dev.modular_avatar.animation
#if MA_VRCSDK3_AVATARS
var avatarDescriptor = context.AvatarDescriptor;
if (!avatarDescriptor) return;
foreach (var layer in avatarDescriptor.baseAnimationLayers)
{
@ -401,7 +416,7 @@ namespace nadena.dev.modular_avatar.animation
{
try
{
AssetDatabase.AddObjectToAsset(curClip, _context.AssetContainer);
_context.AssetSaver.SaveAsset(curClip);
}
catch (Exception e)
{

View File

@ -7,7 +7,9 @@ using nadena.dev.ndmf;
using UnityEditor;
using UnityEditor.Animations;
using UnityEngine;
#if MA_VRCSDK3_AVATARS
using VRC.SDK3.Avatars.Components;
#endif
#endregion
@ -89,12 +91,16 @@ namespace nadena.dev.modular_avatar.animation
// HACK: This is a temporary crutch until we rework the entire animator services system
public void AddPropertyDefinition(AnimatorControllerParameter paramDef)
{
#if MA_VRCSDK3_AVATARS
if (!_context.AvatarDescriptor) return;
var fx = (AnimatorController)
_context.AvatarDescriptor.baseAnimationLayers
.First(l => l.type == VRCAvatarDescriptor.AnimLayerType.FX)
.animatorController;
fx.parameters = fx.parameters.Concat(new[] { paramDef }).ToArray();
#endif
}
public string GetActiveSelfProxy(GameObject obj)

View File

@ -53,6 +53,8 @@ namespace nadena.dev.modular_avatar.animation
// This helps reduce the risk that we'll accidentally modify the original assets.
#if MA_VRCSDK3_AVATARS
if (!context.AvatarDescriptor) return;
context.AvatarDescriptor.baseAnimationLayers =
CloneLayers(context, context.AvatarDescriptor.baseAnimationLayers);
context.AvatarDescriptor.specialAnimationLayers =

View File

@ -78,7 +78,7 @@ namespace nadena.dev.modular_avatar.animation
_combined = new AnimatorController();
if (context.AssetContainer != null && EditorUtility.IsPersistent(context.AssetContainer))
{
AssetDatabase.AddObjectToAsset(_combined, context.AssetContainer);
context.AssetSaver.SaveAsset(_combined);
}
_combined.name = assetName;
@ -191,7 +191,7 @@ namespace nadena.dev.modular_avatar.animation
EditorUtility.CopySerialized(t, newTransition);
if (_context.AssetContainer != null)
{
AssetDatabase.AddObjectToAsset(newTransition, _context.AssetContainer);
_context.AssetSaver.SaveAsset(newTransition);
}
t = newTransition;
}
@ -573,6 +573,8 @@ namespace nadena.dev.modular_avatar.animation
private AnimatorStateMachine mapStateMachine(string basePath, AnimatorStateMachine layerStateMachine)
{
if (layerStateMachine == null) return null;
var cacheKey = new KeyValuePair<string, AnimatorStateMachine>(basePath, layerStateMachine);
if (_stateMachines.TryGetValue(cacheKey, out var asm))

View File

@ -14,6 +14,7 @@ namespace nadena.dev.modular_avatar.animation
internal class DeepClone
{
private BuildContext _context;
private bool _isSaved;
private UnityObject _combined;
@ -21,6 +22,7 @@ namespace nadena.dev.modular_avatar.animation
public DeepClone(BuildContext context)
{
_context = context;
_isSaved = context.AssetContainer != null && EditorUtility.IsPersistent(context.AssetContainer);
_combined = context.AssetContainer;
}
@ -33,6 +35,8 @@ namespace nadena.dev.modular_avatar.animation
if (original == null) return null;
if (cloneMap == null) cloneMap = new Dictionary<UnityObject, UnityObject>();
using var scope = _context.OpenSerializationScope();
Func<UnityObject, UnityObject> visitor = null;
if (basePath != null)
{
@ -96,14 +100,12 @@ namespace nadena.dev.modular_avatar.animation
if (_isSaved && !EditorUtility.IsPersistent(obj))
{
AssetDatabase.AddObjectToAsset(obj, _combined);
scope.SaveAsset(obj);
}
return (T)obj;
}
var ctor = original.GetType().GetConstructor(Type.EmptyTypes);
if (ctor == null || original is ScriptableObject)
{
@ -120,7 +122,7 @@ namespace nadena.dev.modular_avatar.animation
if (_isSaved)
{
AssetDatabase.AddObjectToAsset(obj, _combined);
scope.SaveAsset(obj);
}
SerializedObject so = new SerializedObject(obj);
@ -233,7 +235,7 @@ namespace nadena.dev.modular_avatar.animation
newClip.name = "rebased " + clip.name;
if (_isSaved)
{
AssetDatabase.AddObjectToAsset(newClip, _combined);
_context.AssetSaver.SaveAsset(newClip);
}
foreach (var binding in AnimationUtility.GetCurveBindings(clip))

View File

@ -1,9 +1,12 @@
using System.Linq;
#if MA_VRCSDK3_AVATARS
using System.Linq;
using nadena.dev.modular_avatar.core.editor;
using nadena.dev.ndmf;
using UnityEditor;
using UnityEditor.Animations;
using UnityEngine;
using VRC.SDK3.Avatars.Components;
using BuildContext = nadena.dev.ndmf.BuildContext;
namespace nadena.dev.modular_avatar.animation
{
@ -23,11 +26,16 @@ namespace nadena.dev.modular_avatar.animation
if (fx == null) return;
var nullMotion = new AnimationClip();
nullMotion.name = "NullMotion";
var blendTree = new BlendTree();
blendTree.blendType = BlendTreeType.Direct;
blendTree.useAutomaticThresholds = false;
blendTree.children = asc.BoundReadableProperties.Select(GenerateDelayChild).ToArray();
blendTree.children = asc.BoundReadableProperties
.Select(prop => GenerateDelayChild(nullMotion, prop))
.ToArray();
var asm = new AnimatorStateMachine();
var state = new AnimatorState();
@ -52,9 +60,24 @@ namespace nadena.dev.modular_avatar.animation
defaultWeight = 1,
blendingMode = AnimatorLayerBlendingMode.Override
}).ToArray();
// Ensure the initial state of readable props matches the actual state of the gameobject
var parameters = fx.parameters;
var paramToIndex = parameters.Select((p, i) => (p, i)).ToDictionary(x => x.p.name, x => x.i);
foreach (var (binding, prop) in asc.BoundReadableProperties)
{
var obj = asc.PathMappings.PathToObject(binding.path);
if (obj != null && paramToIndex.TryGetValue(prop, out var index))
{
parameters[index].defaultFloat = obj.activeSelf ? 1 : 0;
}
}
fx.parameters = parameters;
}
private ChildMotion GenerateDelayChild((EditorCurveBinding, string) binding)
private ChildMotion GenerateDelayChild(Motion nullMotion, (EditorCurveBinding, string) binding)
{
var ecb = binding.Item1;
var prop = binding.Item2;
@ -64,12 +87,43 @@ namespace nadena.dev.modular_avatar.animation
curve.AddKey(0, 1);
AnimationUtility.SetEditorCurve(motion, ecb, curve);
// Occasionally, we'll have a very small value pop up, probably due to FP errors.
// To correct for this, instead of directly using the property in the direct blend tree,
// we'll use a 1D blend tree to give ourselves a buffer.
var bufferBlendTree = new BlendTree();
bufferBlendTree.blendType = BlendTreeType.Simple1D;
bufferBlendTree.useAutomaticThresholds = false;
bufferBlendTree.blendParameter = prop;
bufferBlendTree.children = new[]
{
new ChildMotion
{
motion = nullMotion,
timeScale = 1,
threshold = 0
},
new ChildMotion
{
motion = nullMotion,
timeScale = 1,
threshold = 0.01f
},
new ChildMotion
{
motion = motion,
timeScale = 1,
threshold = 1
}
};
return new ChildMotion
{
motion = motion,
directBlendParameter = prop,
motion = bufferBlendTree,
directBlendParameter = MergeBlendTreePass.ALWAYS_ONE,
timeScale = 1
};
}
}
}
}
#endif

View File

@ -368,21 +368,26 @@ namespace nadena.dev.modular_avatar.animation
}
Profiler.EndSample();
var layers = context.AvatarDescriptor.baseAnimationLayers
.Concat(context.AvatarDescriptor.specialAnimationLayers);
Profiler.BeginSample("ApplyMappingsToAvatarMasks");
foreach (var layer in layers)
#if MA_VRCSDK3_AVATARS
if (context.AvatarDescriptor)
{
ApplyMappingsToAvatarMask(layer.mask);
var layers = context.AvatarDescriptor.baseAnimationLayers
.Concat(context.AvatarDescriptor.specialAnimationLayers);
if (layer.animatorController is AnimatorController ac)
// By this point, all AnimationOverrideControllers have been collapsed into an ephemeral
// AnimatorController so we can safely modify the controller in-place.
foreach (var acLayer in ac.layers)
ApplyMappingsToAvatarMask(acLayer.avatarMask);
Profiler.BeginSample("ApplyMappingsToAvatarMasks");
foreach (var layer in layers)
{
ApplyMappingsToAvatarMask(layer.mask);
if (layer.animatorController is AnimatorController ac)
// By this point, all AnimationOverrideControllers have been collapsed into an ephemeral
// AnimatorController so we can safely modify the controller in-place.
foreach (var acLayer in ac.layers)
ApplyMappingsToAvatarMask(acLayer.avatarMask);
}
Profiler.EndSample();
}
Profiler.EndSample();
#endif
Profiler.EndSample();
}

View File

@ -1,4 +1,5 @@
#region
#if MA_VRCSDK3_AVATARS
#region
using System;
using System.Collections.Immutable;
@ -57,4 +58,5 @@ namespace nadena.dev.modular_avatar.core.editor
}
}
}
}
}
#endif

View File

@ -71,7 +71,7 @@ namespace nadena.dev.modular_avatar.core.editor
{
if (!SaveImmediate || AssetDatabase.IsMainAsset(obj) || AssetDatabase.IsSubAsset(obj)) return;
AssetDatabase.AddObjectToAsset(obj, AssetContainer);
PluginBuildContext.AssetSaver.SaveAsset(obj);
}
public AnimatorController CreateAnimator(AnimatorController toClone = null)

View File

@ -19,6 +19,8 @@ namespace nadena.dev.modular_avatar.core.editor
internal static void FixupExpressionsMenu(BuildContext context)
{
if (!context.AvatarDescriptor) return;
context.AvatarDescriptor.customExpressions = true;
var expressionsMenu = context.AvatarDescriptor.expressionsMenu;

View File

@ -11,6 +11,8 @@ namespace nadena.dev.modular_avatar.core.editor.HarmonyPatches
{
internal class PatchLoader
{
private const string HarmonyId = "nadena.dev.modular_avatar";
private static readonly Action<Harmony>[] patches = new Action<Harmony>[]
{
//HierarchyViewPatches.Patch,
@ -19,7 +21,7 @@ namespace nadena.dev.modular_avatar.core.editor.HarmonyPatches
[InitializeOnLoadMethod]
static void ApplyPatches()
{
var harmony = new Harmony("nadena.dev.modular_avatar");
var harmony = new Harmony(HarmonyId);
foreach (var patch in patches)
{
@ -33,7 +35,7 @@ namespace nadena.dev.modular_avatar.core.editor.HarmonyPatches
}
}
AssemblyReloadEvents.beforeAssemblyReload += () => { harmony.UnpatchAll(); };
AssemblyReloadEvents.beforeAssemblyReload += () => { harmony.UnpatchAll(HarmonyId); };
}
}
}

View File

@ -231,6 +231,9 @@ namespace nadena.dev.modular_avatar.core.editor
};
internal static readonly Regex Regex_VRM_Bone = new Regex(@"^([LRC])_(.*)$");
internal static ImmutableHashSet<string> AllBoneNames =
boneNamePatterns.SelectMany(x => x).Select(NormalizeName).ToImmutableHashSet();
internal static string NormalizeName(string name)
{
@ -243,6 +246,14 @@ namespace nadena.dev.modular_avatar.core.editor
internal static readonly ImmutableDictionary<string, List<HumanBodyBones>> NameToBoneMap;
internal static readonly ImmutableDictionary<HumanBodyBones, ImmutableList<string>> BoneToNameMap;
[InitializeOnLoadMethod]
private static void InsertboneNamePatternsToRuntime()
{
ModularAvatarMergeArmature.boneNamePatterns = boneNamePatterns;
ModularAvatarMergeArmature.AllBoneNames = AllBoneNames;
ModularAvatarMergeArmature.NormalizeBoneName = NormalizeName;
}
static HeuristicBoneMapper()
{
var pat_end_side = new Regex(@"[_\.]([LR])$");
@ -306,7 +317,9 @@ namespace nadena.dev.modular_avatar.core.editor
GameObject src,
GameObject newParent,
List<Transform> skipped = null,
HashSet<Transform> unassigned = null
HashSet<Transform> unassigned = null,
Animator avatarAnimator = null,
Dictionary<Transform, HumanBodyBones> outfitHumanoidBones = null
)
{
Dictionary<Transform, Transform> mappings = new Dictionary<Transform, Transform>();
@ -355,21 +368,65 @@ namespace nadena.dev.modular_avatar.core.editor
var childName = child.gameObject.name;
var targetObjectName = childName.Substring(config.prefix.Length,
childName.Length - config.prefix.Length - config.suffix.Length);
if (!NameToBoneMap.TryGetValue(
NormalizeName(targetObjectName), out var bodyBones))
List<HumanBodyBones> bodyBones = null;
var isMapped = false;
if (outfitHumanoidBones != null && outfitHumanoidBones.TryGetValue(child, out var outfitHumanoidBone))
{
if (avatarAnimator != null)
{
var avatarBone = avatarAnimator.GetBoneTransform(outfitHumanoidBone);
if (avatarBone != null && unassigned.Contains(avatarBone))
{
mappings[child] = avatarBone;
unassigned.Remove(avatarBone);
lcNameToXform.Remove(NormalizeName(avatarBone.gameObject.name));
isMapped = true;
} else {
bodyBones = new List<HumanBodyBones> { outfitHumanoidBone };
}
} else {
bodyBones = new List<HumanBodyBones>() { outfitHumanoidBone };
}
}
if (!isMapped && bodyBones == null && !NameToBoneMap.TryGetValue(
NormalizeName(targetObjectName), out bodyBones))
{
continue;
}
foreach (var otherName in bodyBones.SelectMany(bone => BoneToNameMap[bone]))
if (!isMapped)
{
if (lcNameToXform.TryGetValue(otherName, out var targetObject))
foreach (var bodyBone in bodyBones)
{
mappings[child] = targetObject;
unassigned.Remove(targetObject);
lcNameToXform.Remove(otherName.ToLowerInvariant());
break;
if (avatarAnimator != null)
{
var avatarBone = avatarAnimator.GetBoneTransform(bodyBone);
if (avatarBone != null && unassigned.Contains(avatarBone))
{
mappings[child] = avatarBone;
unassigned.Remove(avatarBone);
lcNameToXform.Remove(NormalizeName(avatarBone.gameObject.name));
isMapped = true;
break;
}
}
}
}
if (!isMapped)
{
foreach (var otherName in bodyBones.SelectMany(bone => BoneToNameMap[bone]))
{
if (lcNameToXform.TryGetValue(otherName, out var targetObject))
{
mappings[child] = targetObject;
unassigned.Remove(targetObject);
lcNameToXform.Remove(otherName.ToLowerInvariant());
isMapped = true;
break;
}
}
}
@ -388,7 +445,7 @@ namespace nadena.dev.modular_avatar.core.editor
return mappings;
}
internal static void RenameBonesByHeuristic(ModularAvatarMergeArmature config, List<Transform> skipped = null)
internal static void RenameBonesByHeuristic(ModularAvatarMergeArmature config, List<Transform> skipped = null, Dictionary<Transform, HumanBodyBones> outfitHumanoidBones = null, Animator avatarAnimator = null)
{
var target = config.mergeTarget.Get(RuntimeUtil.FindAvatarTransformInParents(config.transform));
if (target == null) return;
@ -399,7 +456,7 @@ namespace nadena.dev.modular_avatar.core.editor
void Traverse(Transform src, Transform dst)
{
var mappings = AssignBoneMappings(config, src.gameObject, dst.gameObject, skipped: skipped);
var mappings = AssignBoneMappings(config, src.gameObject, dst.gameObject, skipped: skipped, outfitHumanoidBones: outfitHumanoidBones, avatarAnimator: avatarAnimator);
foreach (var pair in mappings)
{

View File

@ -0,0 +1,106 @@
using System;
using System.Linq;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
namespace nadena.dev.modular_avatar.core.editor
{
internal abstract class DragAndDropManipulator<T> : PointerManipulator where T : Component, IHaveObjReferences
{
private const string DragActiveClassName = "drop-area--drag-active";
public T TargetComponent { get; set; }
protected virtual bool AllowKnownObjects => true;
private Transform _avatarRoot;
private GameObject[] _draggingObjects = Array.Empty<GameObject>();
public DragAndDropManipulator(VisualElement targetElement, T targetComponent)
{
target = targetElement;
TargetComponent = targetComponent;
}
protected sealed override void RegisterCallbacksOnTarget()
{
target.RegisterCallback<DragEnterEvent>(OnDragEnter);
target.RegisterCallback<DragLeaveEvent>(OnDragLeave);
target.RegisterCallback<DragExitedEvent>(OnDragExited);
target.RegisterCallback<DragUpdatedEvent>(OnDragUpdated);
target.RegisterCallback<DragPerformEvent>(OnDragPerform);
}
protected sealed override void UnregisterCallbacksFromTarget()
{
target.UnregisterCallback<DragEnterEvent>(OnDragEnter);
target.UnregisterCallback<DragLeaveEvent>(OnDragLeave);
target.UnregisterCallback<DragExitedEvent>(OnDragExited);
target.UnregisterCallback<DragUpdatedEvent>(OnDragUpdated);
target.UnregisterCallback<DragPerformEvent>(OnDragPerform);
}
private void OnDragEnter(DragEnterEvent _)
{
if (TargetComponent == null) return;
_avatarRoot = RuntimeUtil.FindAvatarTransformInParents(TargetComponent.transform);
if (_avatarRoot == null) return;
var knownObjects = TargetComponent.GetObjectReferences().Select(x => x.Get(TargetComponent)).ToHashSet();
_draggingObjects = DragAndDrop.objectReferences.OfType<GameObject>()
.Where(x => AllowKnownObjects || !knownObjects.Contains(x))
.Where(x => RuntimeUtil.FindAvatarTransformInParents(x.transform) == _avatarRoot)
.Where(FilterGameObject)
.ToArray();
if (_draggingObjects.Length == 0) return;
target.AddToClassList(DragActiveClassName);
}
private void OnDragLeave(DragLeaveEvent _)
{
_draggingObjects = Array.Empty<GameObject>();
target.RemoveFromClassList(DragActiveClassName);
}
private void OnDragExited(DragExitedEvent _)
{
_draggingObjects = Array.Empty<GameObject>();
target.RemoveFromClassList(DragActiveClassName);
}
private void OnDragUpdated(DragUpdatedEvent _)
{
if (TargetComponent == null) return;
if (_avatarRoot == null) return;
if (_draggingObjects.Length == 0) return;
DragAndDrop.visualMode = DragAndDropVisualMode.Generic;
}
private void OnDragPerform(DragPerformEvent _)
{
if (TargetComponent == null) return;
if (_avatarRoot == null) return;
if (_draggingObjects.Length == 0) return;
AddObjectReferences(_draggingObjects
.Select(x =>
{
var reference = new AvatarObjectReference();
reference.Set(x);
return reference;
})
.ToArray());
}
protected virtual bool FilterGameObject(GameObject obj)
{
return true;
}
protected abstract void AddObjectReferences(AvatarObjectReference[] references);
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 528c660b56905844ea2f88bc73837e9f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,4 +1,5 @@
using UnityEditor;
#if MA_VRCSDK3_AVATARS
using UnityEditor;
namespace nadena.dev.modular_avatar.core.editor
{
@ -45,3 +46,5 @@ namespace nadena.dev.modular_avatar.core.editor
}
}
}
#endif

View File

@ -16,6 +16,7 @@ namespace nadena.dev.modular_avatar.core.editor.ShapeChanger
[SerializeField] private StyleSheet uss;
[SerializeField] private VisualTreeAsset uxml;
private DragAndDropManipulator _dragAndDropManipulator;
protected override void OnInnerInspectorGUI()
{
@ -37,7 +38,44 @@ namespace nadena.dev.modular_avatar.core.editor.ShapeChanger
listView.showBoundCollectionSize = false;
listView.virtualizationMethod = CollectionVirtualizationMethod.DynamicHeight;
_dragAndDropManipulator = new DragAndDropManipulator(root.Q("group-box"), target as ModularAvatarMaterialSetter);
return root;
}
private void OnEnable()
{
if (_dragAndDropManipulator != null)
_dragAndDropManipulator.TargetComponent = target as ModularAvatarMaterialSetter;
}
private class DragAndDropManipulator : DragAndDropManipulator<ModularAvatarMaterialSetter>
{
public DragAndDropManipulator(VisualElement targetElement, ModularAvatarMaterialSetter targetComponent)
: base(targetElement, targetComponent) { }
protected override bool FilterGameObject(GameObject obj)
{
if (obj.TryGetComponent<Renderer>(out var renderer))
{
return renderer.sharedMaterials.Length > 0;
}
return false;
}
protected override void AddObjectReferences(AvatarObjectReference[] references)
{
Undo.RecordObject(TargetComponent, "Add Material Switch Objects");
foreach (var reference in references)
{
var materialSwitchObject = new MaterialSwitchObject { Object = reference, MaterialIndex = 0 };
TargetComponent.Objects.Add(materialSwitchObject);
}
EditorUtility.SetDirty(TargetComponent);
PrefabUtility.RecordPrefabInstancePropertyModifications(TargetComponent);
}
}
}
}

View File

@ -62,3 +62,13 @@
#f-material {
flex-grow: 1;
}
.drop-area--drag-active {
background-color: rgba(0, 127, 255, 0.2);
}
.drop-area--drag-active .unity-scroll-view,
.drop-area--drag-active .unity-list-view__footer,
.drop-area--drag-active .unity-list-view__reorderable-item {
background-color: rgba(0, 0, 0, 0.0);
}

View File

@ -33,6 +33,29 @@ namespace nadena.dev.modular_avatar.core.editor
private Dictionary<VRCExpressionsMenu, List<ModularAvatarMenuInstaller>> _menuInstallersMap;
private static Editor _cachedEditor;
[InitializeOnLoadMethod]
private static void Init()
{
ModularAvatarMenuInstaller._openSelectMenu = OpenSelectInstallTargetMenu;
}
private static void OpenSelectInstallTargetMenu(ModularAvatarMenuInstaller installer)
{
CreateCachedEditor(installer, typeof(MenuInstallerEditor), ref _cachedEditor);
var editor = (MenuInstallerEditor)_cachedEditor;
editor.OnEnable();
var serializedObject = editor.serializedObject;
var installTo = serializedObject.FindProperty(nameof(ModularAvatarMenuInstaller.installTargetMenu));
var root = editor.FindCommonAvatar();
editor.OpenSelectMenu(root, installTo);
}
private void OnEnable()
{
_installer = (ModularAvatarMenuInstaller) target;
@ -215,74 +238,7 @@ namespace nadena.dev.modular_avatar.core.editor
var avatar = commonAvatar;
if (avatar != null && InstallTargets.Count == 1 && GUILayout.Button(G("menuinstall.selectmenu")))
{
AvMenuTreeViewWindow.Show(avatar, _installer, menu =>
{
if (InstallTargets.Count != 1 || menu == InstallTargets[0]) return;
if (InstallTargets[0] is ModularAvatarMenuInstallTarget oldTarget && oldTarget != null)
{
DestroyInstallTargets();
}
if (menu is ValueTuple<object, object> vt) // TODO: This should be a named type...
{
// Menu, ContextCallback
menu = vt.Item1;
}
if (menu is ModularAvatarMenuItem item)
{
if (item.MenuSource == SubmenuSource.MenuAsset)
{
menu = item.Control.subMenu;
}
else
{
var menuParent = item.menuSource_otherObjectChildren != null
? item.menuSource_otherObjectChildren
: item.gameObject;
menu = new MenuNodesUnder(menuParent);
}
}
else if (menu is ModularAvatarMenuGroup group)
{
if (group.targetObject != null) menu = new MenuNodesUnder(group.targetObject);
else menu = new MenuNodesUnder(group.gameObject);
}
if (menu is VRCExpressionsMenu expMenu)
{
if (expMenu == avatar.expressionsMenu) installTo.objectReferenceValue = null;
else installTo.objectReferenceValue = expMenu;
}
else if (menu is RootMenu)
{
installTo.objectReferenceValue = null;
}
else if (menu is MenuNodesUnder nodesUnder)
{
installTo.objectReferenceValue = null;
foreach (var target in targets.Cast<Component>().OrderBy(ObjectHierarchyOrder))
{
var installer = (ModularAvatarMenuInstaller) target;
var child = new GameObject();
Undo.RegisterCreatedObjectUndo(child, "Set install target");
child.transform.SetParent(nodesUnder.root.transform, false);
child.name = installer.gameObject.name;
var targetComponent = child.AddComponent<ModularAvatarMenuInstallTarget>();
targetComponent.installer = installer;
EditorGUIUtility.PingObject(child);
}
}
serializedObject.ApplyModifiedProperties();
VirtualMenu.InvalidateCaches();
Repaint();
});
OpenSelectMenu(avatar, installTo);
}
}
@ -368,7 +324,79 @@ namespace nadena.dev.modular_avatar.core.editor
serializedObject.ApplyModifiedProperties();
Localization.ShowLanguageUI();
ShowLanguageUI();
}
private void OpenSelectMenu(VRCAvatarDescriptor avatar, SerializedProperty installTo)
{
AvMenuTreeViewWindow.Show(avatar, _installer, menu =>
{
if (InstallTargets.Count != 1 || menu == InstallTargets[0]) return;
if (InstallTargets[0] is ModularAvatarMenuInstallTarget oldTarget && oldTarget != null)
{
DestroyInstallTargets();
}
if (menu is ValueTuple<object, object> vt) // TODO: This should be a named type...
{
// Menu, ContextCallback
menu = vt.Item1;
}
if (menu is ModularAvatarMenuItem item)
{
if (item.MenuSource == SubmenuSource.MenuAsset)
{
menu = item.Control.subMenu;
}
else
{
var menuParent = item.menuSource_otherObjectChildren != null
? item.menuSource_otherObjectChildren
: item.gameObject;
menu = new MenuNodesUnder(menuParent);
}
}
else if (menu is ModularAvatarMenuGroup group)
{
if (group.targetObject != null) menu = new MenuNodesUnder(group.targetObject);
else menu = new MenuNodesUnder(group.gameObject);
}
if (menu is VRCExpressionsMenu expMenu)
{
if (expMenu == avatar.expressionsMenu) installTo.objectReferenceValue = null;
else installTo.objectReferenceValue = expMenu;
}
else if (menu is RootMenu)
{
installTo.objectReferenceValue = null;
}
else if (menu is MenuNodesUnder nodesUnder)
{
installTo.objectReferenceValue = null;
foreach (var target in targets.Cast<Component>().OrderBy(ObjectHierarchyOrder))
{
var installer = (ModularAvatarMenuInstaller)target;
var child = new GameObject();
Undo.RegisterCreatedObjectUndo(child, "Set install target");
child.transform.SetParent(nodesUnder.root.transform, false);
child.name = installer.gameObject.name;
var targetComponent = child.AddComponent<ModularAvatarMenuInstallTarget>();
targetComponent.installer = installer;
EditorGUIUtility.PingObject(child);
}
}
serializedObject.ApplyModifiedProperties();
VirtualMenu.InvalidateCaches();
Repaint();
});
}
private string ObjectHierarchyOrder(Component arg)
@ -415,6 +443,9 @@ namespace nadena.dev.modular_avatar.core.editor
var group = installer.gameObject.AddComponent<ModularAvatarMenuGroup>();
var menuRoot = new GameObject();
menuRoot.name = "Menu";
group.targetObject = menuRoot;
Undo.RegisterCreatedObjectUndo(menuRoot, "Extract menu");
menuRoot.transform.SetParent(group.transform, false);
foreach (var control in menu.controls)

View File

@ -176,11 +176,16 @@ namespace nadena.dev.modular_avatar.core.editor
}
Dictionary<string, ProvidedParameter> rootParameters = new();
foreach (var param in ParameterIntrospectionCache.GetParametersForObject(parentAvatar.gameObject)
.Where(p => p.Namespace == ParameterNamespace.Animator)
)
rootParameters[param.EffectiveName] = param;
{
if (!string.IsNullOrWhiteSpace(param.EffectiveName))
{
rootParameters[param.EffectiveName] = param;
}
}
var remaps = ParameterIntrospectionCache.GetParameterRemappingsAt(paramRef);
foreach (var remap in remaps)
@ -366,10 +371,9 @@ namespace nadena.dev.modular_avatar.core.editor
EditorGUILayout.BeginVertical();
if (_type.hasMultipleDifferentValues) return;
VRCExpressionsMenu.Control.ControlType type =
(VRCExpressionsMenu.Control.ControlType) Enum
.GetValues(typeof(VRCExpressionsMenu.Control.ControlType))
.GetValue(_type.enumValueIndex);
var controlTypeArray = Enum.GetValues(typeof(VRCExpressionsMenu.Control.ControlType));
var index = Math.Clamp(_type.enumValueIndex, 0, controlTypeArray.Length - 1);
var type = (VRCExpressionsMenu.Control.ControlType)controlTypeArray.GetValue(index);
switch (type)
{
@ -582,7 +586,12 @@ namespace nadena.dev.modular_avatar.core.editor
// But, we do want to see if _any_ are default.
var anyIsDefault = _prop_isDefault.hasMultipleDifferentValues || _prop_isDefault.boolValue;
var mixedIsDefault = multipleSelections && anyIsDefault;
using (new EditorGUI.DisabledScope(multipleSelections || isDefaultByKnownParam != null))
var allAreAutoParams = !_parameterName.hasMultipleDifferentValues &&
string.IsNullOrWhiteSpace(_parameterName.stringValue);
using (new EditorGUI.DisabledScope((!allAreAutoParams && multipleSelections) ||
isDefaultByKnownParam != null))
{
EditorGUI.BeginChangeCheck();
DrawHorizontalToggleProp(_prop_isDefault, G("menuitem.prop.is_default"), mixedIsDefault,
@ -711,6 +720,9 @@ namespace nadena.dev.modular_avatar.core.editor
var myMenuItem = serializedObject.targetObject as ModularAvatarMenuItem;
if (myMenuItem == null) return null;
var avatarRoot = RuntimeUtil.FindAvatarInParents(myMenuItem.gameObject.transform);
if (avatarRoot == null) return null;
var myParameterName = myMenuItem.Control.parameter.name;
if (string.IsNullOrEmpty(myParameterName)) return new List<ModularAvatarMenuItem>();
@ -718,7 +730,6 @@ namespace nadena.dev.modular_avatar.core.editor
if (myMappings.TryGetValue((ParameterNamespace.Animator, myParameterName), out var myReplacement))
myParameterName = myReplacement.ParameterName;
var avatarRoot = RuntimeUtil.FindAvatarInParents(myMenuItem.gameObject.transform);
var siblings = new List<ModularAvatarMenuItem>();
foreach (var otherMenuItem in avatarRoot.GetComponentsInChildren<ModularAvatarMenuItem>(true))

View File

@ -1,4 +1,5 @@
using nadena.dev.modular_avatar.ui;
#if MA_VRCSDK3_AVATARS
using nadena.dev.modular_avatar.ui;
using UnityEditor;
using UnityEngine;
using VRC.SDK3.Avatars.ScriptableObjects;
@ -62,4 +63,5 @@ namespace nadena.dev.modular_avatar.core.editor
Undo.RegisterCreatedObjectUndo(toggle, "Create Toggle");
}
}
}
}
#endif

View File

@ -84,6 +84,7 @@ namespace nadena.dev.modular_avatar.core.editor
}
private bool posResetOptionFoldout = false;
private bool posReset_convertATPose = true;
private bool posReset_adjustRotation = false;
private bool posReset_adjustScale = false;
private bool posReset_heuristicRootScale = true;
@ -99,7 +100,7 @@ namespace nadena.dev.modular_avatar.core.editor
{
serializedObject.ApplyModifiedProperties();
if (target.mergeTargetObject != null && priorMergeTarget == null
if (target.mergeTargetObject != null && priorMergeTarget != target.mergeTargetObject
&& string.IsNullOrEmpty(target.prefix)
&& string.IsNullOrEmpty(target.suffix))
{
@ -114,7 +115,27 @@ namespace nadena.dev.modular_avatar.core.editor
{
if (GUILayout.Button(G("merge_armature.adjust_names")))
{
HeuristicBoneMapper.RenameBonesByHeuristic(target);
var avatarRoot = RuntimeUtil.FindAvatarTransformInParents(target.mergeTarget.Get(target).transform);
var avatarAnimator = avatarRoot != null ? avatarRoot.GetComponent<Animator>() : null;
// Search Outfit Root Animator
var outfitRoot = ((ModularAvatarMergeArmature)serializedObject.targetObject).transform;
Animator outfitAnimator = null;
while (outfitRoot != null)
{
if (outfitRoot == avatarRoot)
{
outfitAnimator = null;
break;
}
outfitAnimator = outfitRoot.GetComponent<Animator>();
if (outfitAnimator != null && outfitAnimator.isHuman) break;
outfitAnimator = null;
outfitRoot = outfitRoot.parent;
}
var outfitHumanoidBones = SetupOutfit.GetOutfitHumanoidBones(outfitRoot, outfitAnimator);
HeuristicBoneMapper.RenameBonesByHeuristic(target, outfitHumanoidBones: outfitHumanoidBones, avatarAnimator: avatarAnimator);
}
}
@ -134,14 +155,17 @@ namespace nadena.dev.modular_avatar.core.editor
MessageType.Info
);
posReset_heuristicRootScale = EditorGUILayout.ToggleLeft(
G("merge_armature.reset_pos.heuristic_scale"),
posReset_heuristicRootScale);
posReset_convertATPose = EditorGUILayout.ToggleLeft(
G("merge_armature.reset_pos.convert_atpose"),
posReset_convertATPose);
posReset_adjustRotation = EditorGUILayout.ToggleLeft(
G("merge_armature.reset_pos.adjust_rotation"),
posReset_adjustRotation);
posReset_adjustScale = EditorGUILayout.ToggleLeft(G("merge_armature.reset_pos.adjust_scale"),
posReset_adjustScale);
posReset_heuristicRootScale = EditorGUILayout.ToggleLeft(
G("merge_armature.reset_pos.heuristic_scale"),
posReset_heuristicRootScale);
if (GUILayout.Button(G("merge_armature.reset_pos.execute")))
{
@ -188,6 +212,11 @@ namespace nadena.dev.modular_avatar.core.editor
}
}
if (posReset_convertATPose)
{
SetupOutfit.FixAPose(RuntimeUtil.FindAvatarTransformInParents(mergeTarget.transform).gameObject, mama.transform, false);
}
if (posReset_heuristicRootScale && !suppressRootScale)
{
AdjustRootScale();
@ -279,4 +308,4 @@ namespace nadena.dev.modular_avatar.core.editor
}
}
}
}
}

View File

@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using nadena.dev.modular_avatar.core.ArmatureAwase;
using nadena.dev.ndmf.preview;
using UnityEditor;
using UnityEngine;

View File

@ -35,14 +35,12 @@ namespace nadena.dev.modular_avatar.core.editor.ShapeChanger
ROSimulatorButton.BindRefObject(root, target);
var listView = root.Q<ListView>("Shapes");
_dragAndDropManipulator = new DragAndDropManipulator(listView)
{
TargetComponent = target as ModularAvatarObjectToggle
};
listView.showBoundCollectionSize = false;
listView.virtualizationMethod = CollectionVirtualizationMethod.DynamicHeight;
_dragAndDropManipulator = new DragAndDropManipulator(root.Q("group-box"), target as ModularAvatarObjectToggle);
return root;
}
@ -52,91 +50,25 @@ namespace nadena.dev.modular_avatar.core.editor.ShapeChanger
_dragAndDropManipulator.TargetComponent = target as ModularAvatarObjectToggle;
}
private class DragAndDropManipulator : PointerManipulator
private class DragAndDropManipulator : DragAndDropManipulator<ModularAvatarObjectToggle>
{
public ModularAvatarObjectToggle TargetComponent;
private GameObject[] _nowDragging = Array.Empty<GameObject>();
private Transform _avatarRoot;
public DragAndDropManipulator(VisualElement targetElement, ModularAvatarObjectToggle targetComponent)
: base(targetElement, targetComponent) { }
private readonly VisualElement _parentElem;
protected override bool AllowKnownObjects => false;
public DragAndDropManipulator(VisualElement target)
protected override void AddObjectReferences(AvatarObjectReference[] references)
{
this.target = target;
_parentElem = target.parent;
}
Undo.RecordObject(TargetComponent, "Add Toggled Objects");
protected override void RegisterCallbacksOnTarget()
{
target.RegisterCallback<DragEnterEvent>(OnDragEnter);
target.RegisterCallback<DragLeaveEvent>(OnDragLeave);
target.RegisterCallback<DragPerformEvent>(OnDragPerform);
target.RegisterCallback<DragUpdatedEvent>(OnDragUpdate);
}
protected override void UnregisterCallbacksFromTarget()
{
target.UnregisterCallback<DragEnterEvent>(OnDragEnter);
target.UnregisterCallback<DragLeaveEvent>(OnDragLeave);
target.UnregisterCallback<DragPerformEvent>(OnDragPerform);
target.RegisterCallback<DragUpdatedEvent>(OnDragUpdate);
}
private void OnDragEnter(DragEnterEvent evt)
{
if (TargetComponent == null) return;
_avatarRoot = RuntimeUtil.FindAvatarTransformInParents(TargetComponent.transform);
if (_avatarRoot == null) return;
_nowDragging = DragAndDrop.objectReferences.OfType<GameObject>()
.Where(o => RuntimeUtil.FindAvatarTransformInParents(o.transform) == _avatarRoot)
.ToArray();
if (_nowDragging.Length > 0)
foreach (var reference in references)
{
DragAndDrop.visualMode = DragAndDropVisualMode.Link;
_parentElem.AddToClassList("drop-area--drag-active");
}
}
private void OnDragUpdate(DragUpdatedEvent _)
{
if (_nowDragging.Length > 0) DragAndDrop.visualMode = DragAndDropVisualMode.Link;
}
private void OnDragLeave(DragLeaveEvent evt)
{
_nowDragging = Array.Empty<GameObject>();
_parentElem.RemoveFromClassList("drop-area--drag-active");
}
private void OnDragPerform(DragPerformEvent evt)
{
if (_nowDragging.Length > 0 && TargetComponent != null && _avatarRoot != null)
{
var knownObjs = TargetComponent.Objects.Select(o => o.Object.Get(TargetComponent)).ToHashSet();
Undo.RecordObject(TargetComponent, "Add Toggled Objects");
foreach (var obj in _nowDragging)
{
if (knownObjs.Contains(obj)) continue;
var aor = new AvatarObjectReference();
aor.Set(obj);
var toggledObject = new ToggledObject { Object = aor, Active = !obj.activeSelf };
TargetComponent.Objects.Add(toggledObject);
}
EditorUtility.SetDirty(TargetComponent);
PrefabUtility.RecordPrefabInstancePropertyModifications(TargetComponent);
var toggledObject = new ToggledObject { Object = reference, Active = !reference.Get(TargetComponent).activeSelf };
TargetComponent.Objects.Add(toggledObject);
}
_nowDragging = Array.Empty<GameObject>();
_parentElem.RemoveFromClassList("drop-area--drag-active");
EditorUtility.SetDirty(TargetComponent);
PrefabUtility.RecordPrefabInstancePropertyModifications(TargetComponent);
}
}
}

View File

@ -51,6 +51,12 @@
width: 60px;
}
.drop-area--drag-active > ListView ScrollView {
background-color: rgba(0, 255, 255, 0.1);
.drop-area--drag-active {
background-color: rgba(0, 127, 255, 0.2);
}
.drop-area--drag-active .unity-scroll-view,
.drop-area--drag-active .unity-list-view__footer,
.drop-area--drag-active .unity-list-view__reorderable-item {
background-color: rgba(0, 0, 0, 0.0);
}

View File

@ -5,8 +5,12 @@ using System.Linq;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.UIElements;
using VRC.SDK3.Avatars.ScriptableObjects;
using static nadena.dev.modular_avatar.core.editor.Localization;
using Button = UnityEngine.UIElements.Button;
using Image = UnityEngine.UIElements.Image;
namespace nadena.dev.modular_avatar.core.editor
{
@ -35,6 +39,37 @@ namespace nadena.dev.modular_avatar.core.editor
listView.showBoundCollectionSize = false;
listView.virtualizationMethod = CollectionVirtualizationMethod.DynamicHeight;
listView.selectionType = SelectionType.Multiple;
listView.RegisterCallback<KeyDownEvent>(evt =>
{
if (evt.keyCode == KeyCode.Delete)
{
serializedObject.Update();
var prop = serializedObject.FindProperty("parameters");
var indices = listView.selectedIndices.ToList();
foreach (var index in indices.OrderByDescending(i => i))
{
prop.DeleteArrayElementAtIndex(index);
}
serializedObject.ApplyModifiedProperties();
if (indices.Count == 0)
{
EditorApplication.delayCall += () =>
{
// Works around an issue where the inner text boxes are auto-selected, preventing you from
// just hitting delete over and over
listView.SetSelectionWithoutNotify(indices);
};
}
}
evt.StopPropagation();
}, TrickleDown.NoTrickleDown);
unregisteredListView = root.Q<ListView>("UnregisteredParameters");
@ -128,10 +163,73 @@ namespace nadena.dev.modular_avatar.core.editor
EditorApplication.delayCall += DetectParameters;
}
};
var importProp = root.Q<ObjectField>("p_import");
importProp.RegisterValueChangedCallback(evt =>
{
ImportValues(importProp);
importProp.SetValueWithoutNotify(null);
});
importProp.objectType = typeof(VRCExpressionParameters);
importProp.allowSceneObjects = false;
return root;
}
private void ImportValues(ObjectField importProp)
{
var known = new HashSet<string>();
var target = (ModularAvatarParameters)this.target;
foreach (var parameter in target.parameters)
{
if (!parameter.isPrefix)
{
known.Add(parameter.nameOrPrefix);
}
}
Undo.RecordObject(target, "Import parameters");
var source = (VRCExpressionParameters)importProp.value;
if (source == null)
{
return;
}
foreach (var parameter in source.parameters)
{
if (!known.Contains(parameter.name))
{
ParameterSyncType pst;
switch (parameter.valueType)
{
case VRCExpressionParameters.ValueType.Bool: pst = ParameterSyncType.Bool; break;
case VRCExpressionParameters.ValueType.Float: pst = ParameterSyncType.Float; break;
case VRCExpressionParameters.ValueType.Int: pst = ParameterSyncType.Int; break;
default: pst = ParameterSyncType.Float; break;
}
if (!parameter.networkSynced)
{
pst = ParameterSyncType.NotSynced;
}
target.parameters.Add(new ParameterConfig()
{
internalParameter = false,
nameOrPrefix = parameter.name,
isPrefix = false,
remapTo = "",
syncType = pst,
defaultValue = parameter.defaultValue,
saved = parameter.saved,
});
}
}
}
private void DetectParameters()
{
var known = new HashSet<string>();

View File

@ -20,7 +20,8 @@ namespace nadena.dev.modular_avatar.core.editor
private readonly DropdownField _boolField;
private ParameterSyncType _syncType;
private bool _hasInitialBinding;
public DefaultValueField()
{
// Hidden binding elements
@ -57,28 +58,39 @@ namespace nadena.dev.modular_avatar.core.editor
{
_numberField.style.display = DisplayStyle.Flex;
_boolField.style.display = DisplayStyle.None;
OnUpdateNumberValue(_numberField.value);
OnUpdateNumberValue(_numberField.value, true);
}
else
{
_numberField.style.display = DisplayStyle.None;
_boolField.style.display = DisplayStyle.Flex;
OnUpdateBoolValue(_boolField.value);
OnUpdateBoolValue(_boolField.value, true);
}
}
private void OnUpdateNumberValue(string value)
private void OnUpdateNumberValue(string value, bool implicitUpdate = false)
{
// Upon initial creation, sometimes the OnUpdateSyncType fires before we receive the initial value event.
// In this case, suppress the update to avoid losing data.
if (implicitUpdate && !_hasInitialBinding) return;
var theValue = _defaultValueField.value;
if (string.IsNullOrWhiteSpace(value))
{
_defaultValueField.value = 0;
if (!implicitUpdate)
{
_defaultValueField.value = 0;
}
theValue = _defaultValueField.value;
_hasExplicitDefaultValueField.value = false;
}
else if (float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsed)
&& !float.IsNaN(parsed)
&& !float.IsInfinity(parsed))
{
_defaultValueField.value = _syncType switch
theValue = _defaultValueField.value = _syncType switch
{
ParameterSyncType.Int => Mathf.FloorToInt(Mathf.Clamp(parsed, 0, 255)),
ParameterSyncType.Float => Mathf.Clamp(parsed, -1, 1),
@ -88,11 +100,15 @@ namespace nadena.dev.modular_avatar.core.editor
_hasExplicitDefaultValueField.value = true;
}
UpdateVisibleField(_defaultValueField.value, _hasExplicitDefaultValueField.value);
UpdateVisibleField(theValue, _hasExplicitDefaultValueField.value);
}
private void OnUpdateBoolValue(string value)
private void OnUpdateBoolValue(string value, bool implicitUpdate = false)
{
// Upon initial creation, sometimes the OnUpdateSyncType fires before we receive the initial value event.
// In this case, suppress the update to avoid losing data.
if (implicitUpdate && !_hasInitialBinding) return;
_defaultValueField.value = value == V_True ? 1 : 0;
_hasExplicitDefaultValueField.value = value != V_None;
@ -101,6 +117,8 @@ namespace nadena.dev.modular_avatar.core.editor
private void UpdateVisibleField(float value, bool hasExplicitValue)
{
_hasInitialBinding = true;
if (hasExplicitValue || Mathf.Abs(value) > 0.0000001)
{
_numberField.SetValueWithoutNotify(value.ToString(CultureInfo.InvariantCulture));

View File

@ -81,6 +81,12 @@ namespace nadena.dev.modular_avatar.core.editor.Parameters
updateRemapToPlaceholder();
foreach (var elem in root.Query<TextElement>().Build())
{
// Prevent keypresses from bubbling up
elem.RegisterCallback<KeyDownEvent>(evt => evt.StopPropagation(), TrickleDown.NoTrickleDown);
}
return root;
}

View File

@ -1,5 +1,6 @@
#ListViewContainer {
margin-top: 4px;
max-height: 500px;
}
.horizontal {

View File

@ -12,7 +12,6 @@
show-border="true"
show-foldout-header="false"
name="Parameters"
item-height="100"
binding-path="parameters"
style="flex-grow: 1;"
/>
@ -33,5 +32,7 @@
/>
</ui:Foldout>
<editor:ObjectField name="p_import" label="merge_parameter.ui.importFromAsset" class="ndmf-tr"/>
<ma:LanguageSwitcherElement/>
</UXML>

View File

@ -0,0 +1,39 @@
using System.Diagnostics.CodeAnalysis;
using UnityEditor;
using static nadena.dev.modular_avatar.core.editor.Localization;
namespace nadena.dev.modular_avatar.core.editor
{
[CustomPropertyDrawer(typeof(ModularAvatarRemoveVertexColor.RemoveMode))]
[SuppressMessage("ReSharper", "InconsistentNaming")]
internal class RVCModeDrawer : EnumDrawer<ModularAvatarRemoveVertexColor.RemoveMode>
{
protected override string localizationPrefix => "remove-vertex-color.mode";
}
[CustomEditor(typeof(ModularAvatarRemoveVertexColor))]
internal class RemoveVertexColorEditor : MAEditorBase
{
private SerializedProperty _p_mode;
protected void OnEnable()
{
_p_mode = serializedObject.FindProperty(nameof(ModularAvatarRemoveVertexColor.Mode));
}
protected override void OnInnerInspectorGUI()
{
serializedObject.Update();
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(_p_mode, G("remove-vertex-color.mode"));
if (EditorGUI.EndChangeCheck())
{
serializedObject.ApplyModifiedProperties();
}
ShowLanguageUI();
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: bfcaf601e9f94ba2900e66d66f469037
timeCreated: 1733085477

View File

@ -19,6 +19,7 @@ namespace nadena.dev.modular_avatar.core.editor.ShapeChanger
[SerializeField] private StyleSheet uss;
[SerializeField] private VisualTreeAsset uxml;
private DragAndDropManipulator _dragAndDropManipulator;
private BlendshapeSelectWindow _window;
protected override void OnInnerInspectorGUI()
@ -41,6 +42,8 @@ namespace nadena.dev.modular_avatar.core.editor.ShapeChanger
listView.showBoundCollectionSize = false;
listView.virtualizationMethod = CollectionVirtualizationMethod.DynamicHeight;
_dragAndDropManipulator = new DragAndDropManipulator(root.Q("group-box"), target as ModularAvatarShapeChanger);
// The Add button callback isn't exposed publicly for some reason...
var field_addButton = typeof(BaseListView).GetField("m_AddButton", NonPublic | Instance);
var addButton = (Button)field_addButton.GetValue(listView);
@ -50,6 +53,41 @@ namespace nadena.dev.modular_avatar.core.editor.ShapeChanger
return root;
}
private void OnEnable()
{
if (_dragAndDropManipulator != null)
_dragAndDropManipulator.TargetComponent = target as ModularAvatarShapeChanger;
}
private class DragAndDropManipulator : DragAndDropManipulator<ModularAvatarShapeChanger>
{
public DragAndDropManipulator(VisualElement targetElement, ModularAvatarShapeChanger targetComponent)
: base(targetElement, targetComponent) { }
protected override bool FilterGameObject(GameObject obj)
{
if (obj.TryGetComponent<SkinnedMeshRenderer>(out var smr))
{
return smr.sharedMesh != null && smr.sharedMesh.blendShapeCount > 0;
}
return false;
}
protected override void AddObjectReferences(AvatarObjectReference[] references)
{
Undo.RecordObject(TargetComponent, "Add Changed Shapes");
foreach (var reference in references)
{
var changedShape = new ChangedShape { Object = reference, ShapeName = string.Empty };
TargetComponent.Shapes.Add(changedShape);
}
EditorUtility.SetDirty(TargetComponent);
PrefabUtility.RecordPrefabInstancePropertyModifications(TargetComponent);
}
}
private void OnDisable()
{
if (_window != null) DestroyImmediate(_window);

View File

@ -68,3 +68,13 @@
.change-type-delete #f-value-delete {
display: flex;
}
.drop-area--drag-active {
background-color: rgba(0, 127, 255, 0.2);
}
.drop-area--drag-active .unity-scroll-view,
.drop-area--drag-active .unity-list-view__footer,
.drop-area--drag-active .unity-list-view__reorderable-item {
background-color: rgba(0, 0, 0, 0.0);
}

View File

@ -0,0 +1,101 @@
using System;
using UnityEditor;
using UnityEngine;
using VRC.SDK3.Avatars.ScriptableObjects;
using static nadena.dev.modular_avatar.core.editor.Localization;
namespace nadena.dev.modular_avatar.core.editor
{
[CustomEditor(typeof(ModularAvatarSyncParameterSequence))]
[CanEditMultipleObjects]
public class SyncParameterSequenceEditor : MAEditorBase
{
private SerializedProperty _p_platform;
private SerializedProperty _p_parameters;
private void OnEnable()
{
_p_platform = serializedObject.FindProperty(nameof(ModularAvatarSyncParameterSequence.PrimaryPlatform));
_p_parameters = serializedObject.FindProperty(nameof(ModularAvatarSyncParameterSequence.Parameters));
}
protected override void OnInnerInspectorGUI()
{
serializedObject.Update();
EditorGUI.BeginChangeCheck();
#if MA_VRCSDK3_AVATARS
var disable = false;
#else
bool disable = true;
#endif
// ReSharper disable once ConditionIsAlwaysTrueOrFalse
if (disable)
// ReSharper disable HeuristicUnreachableCode
{
EditorGUILayout.HelpBox(S("general.vrcsdk-required"), MessageType.Warning);
}
// ReSharper restore HeuristicUnreachableCode
// ReSharper disable once ConditionIsAlwaysTrueOrFalse
using (new EditorGUI.DisabledGroupScope(disable))
{
EditorGUILayout.PropertyField(_p_platform, G("sync-param-sequence.platform"));
GUILayout.BeginHorizontal();
var label = G("sync-param-sequence.parameters");
var sizeCalc = EditorStyles.objectField.CalcSize(label);
EditorGUILayout.PropertyField(_p_parameters, label);
if (GUILayout.Button(G("sync-param-sequence.create-asset"),
GUILayout.ExpandWidth(false),
GUILayout.Height(sizeCalc.y)
))
{
CreateParameterAsset();
}
GUILayout.EndHorizontal();
}
if (EditorGUI.EndChangeCheck())
{
serializedObject.ApplyModifiedProperties();
}
ShowLanguageUI();
}
private void CreateParameterAsset()
{
#if MA_VRCSDK3_AVATARS
Transform avatarRoot = null;
if (targets.Length == 1)
{
avatarRoot =
RuntimeUtil.FindAvatarTransformInParents(((ModularAvatarSyncParameterSequence)target).transform);
}
var assetName = "Avatar";
if (avatarRoot != null) assetName = avatarRoot.gameObject.name;
assetName += " SyncedParams";
var file = EditorUtility.SaveFilePanelInProject("Create new parameter asset", assetName, "asset",
"Create a new parameter asset");
var obj = CreateInstance<VRCExpressionParameters>();
obj.parameters = Array.Empty<VRCExpressionParameters.Parameter>();
obj.isEmpty = true;
AssetDatabase.CreateAsset(obj, file);
Undo.RegisterCreatedObjectUndo(obj, "Create parameter asset");
_p_parameters.objectReferenceValue = obj;
serializedObject.ApplyModifiedProperties();
#endif
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: bf6030b7fa704997885767897d1acba0
timeCreated: 1733090792

View File

@ -51,6 +51,7 @@
"merge_parameter.ui.add_button": "Add",
"merge_parameter.ui.details": "Parameter Configuration",
"merge_parameter.ui.overrideAnimatorDefaults": "Override Animator Defaults",
"merge_parameter.ui.importFromAsset": "Import from asset",
"merge_armature.merge_target": "Merge Target",
"merge_armature.merge_target.tooltip": "The armature (or subtree) to merge this object into",
"merge_armature.prefix": "Prefix",
@ -86,6 +87,7 @@
"merge_armature.lockmode.bidirectional.body": "The base armature and the merged armature will always have the same position. This is useful when creating animations that are meant to target the base armature. In order to activate this, your armatures must already be in the exact same position.",
"merge_armature.reset_pos": "Reset position to base avatar",
"merge_armature.reset_pos.info": "This command will force the position of all bones in the outfit to match that of the base avatar. This can be helpful as a starting point for installing outfits not set up for your current avatar.",
"merge_armature.reset_pos.convert_atpose": "Convert A-Pose/T-Pose to match base avatar",
"merge_armature.reset_pos.adjust_rotation": "Also set rotation to base avatar",
"merge_armature.reset_pos.adjust_scale": "Also set local scale to base avatar",
"merge_armature.reset_pos.execute": "Do it!",
@ -149,6 +151,9 @@
"error.rename_params.default_value_conflict:hint": "To avoid unpredictable behavior, leave the default value field blank in all but on MA Parameters component. If multiple values are present, Modular Avatar will select the first default value specified in the hierarchy order.",
"error.replace_object.null_target": "[MA-0008] No target specified",
"error.replace_object.null_target:hint": "Replace object needs a target object to replace. Try setting one.",
"error.replace_object.replacing_replacement": "[MA-0009] The same target object cannot be specified in multiple Replace Object components",
"error.replace_object.parent_of_target": "[MA-0010] The target object cannot be a parent of this object",
"error.singleton": "[MA-0011] Only one instance of {0} is allowed in an avatar",
"validation.blendshape_sync.no_local_renderer": "[MA-1000] No renderer found on this object",
"validation.blendshape_sync.no_local_renderer:hint": "Blendshape Sync acts on a Skinned Mesh Renderer on the same GameObject. Did you attach it to the right object?",
"validation.blendshape_sync.no_local_mesh": "[MA-1001] No mesh found on the renderer on this object",
@ -281,7 +286,19 @@
"ro_sim.effect_group.rule_inverted": "This rule is inverted",
"ro_sim.effect_group.rule_inverted.tooltip": "This rule will be applied when one of its conditions is NOT met",
"ro_sim.effect_group.conditions": "Conditions",
"menuitem.label.long_name.tooltip": "Use a long name which may contain rich text and line breaks.",
"menuitem.label.gameobject_name.tooltip": "Use the GameObject name."
"menuitem.label.gameobject_name.tooltip": "Use the GameObject name.",
"remove-vertex-color.mode": "Mode",
"remove-vertex-color.mode.Remove": "Remove Vertex Colors",
"remove-vertex-color.mode.DontRemove": "Keep Vertex Colors",
"general.vrcsdk-required": "This component requires the VRCSDK to function.",
"sync-param-sequence.platform": "Primary Platform",
"sync-param-sequence.platform.tooltip": "When building for this platform, Modular Avatar will record all expression parameters for use on other platform builds",
"sync-param-sequence.parameters": "Common parameters asset",
"sync-param-sequence.parameters.tooltip": "The asset to store common parameters in. Do not use the same Expression Parameters that you have set in your avatar descriptor.",
"sync-param-sequence.create-asset": "New",
"sync-param-sequence.create-asset.tooltip": "Creates a new expression parameters asset"
}

View File

@ -47,6 +47,7 @@
"merge_parameter.ui.add_button": "追加",
"merge_parameter.ui.details": "パラメーターの詳細設定",
"merge_parameter.ui.overrideAnimatorDefaults": "アニメーターでの初期値を設定",
"merge_parameter.ui.importFromAsset": "アセットからインポートする",
"merge_armature.merge_target": "統合先",
"merge_armature.merge_target.tooltip": "このオブジェクトを統合先のアーマチュアに統合します",
"merge_armature.prefix": "接頭辞",
@ -82,6 +83,7 @@
"merge_armature.lockmode.bidirectional.body": "アバターと統合されるアーマチュアは常に同じ位置になります。元のアバターを操作するアニメーションを作る時に便利かもしれません。有効にするためには、統合されるアーマチュアの位置を統合先と同じにしておく必要があります。",
"merge_armature.reset_pos": "位置を元アバターに合わせてリセット",
"merge_armature.reset_pos.info": "衣装のボーンの位置をアバターのボーンの位置に合わせます。非対応衣装を導入する際、アバウトに位置を合わせるのに便利です。",
"merge_armature.reset_pos.convert_atpose": "Aポーズ/Tポーズを合わせる",
"merge_armature.reset_pos.adjust_rotation": "回転も合わせる",
"merge_armature.reset_pos.adjust_scale": "スケールも合わせる",
"merge_armature.reset_pos.execute": "実行",
@ -145,6 +147,9 @@
"error.rename_params.default_value_conflict:hint": "予測不可能な動作を避けるため、MA Parametersコンポーネントの初期値フィールドはパラメーター名毎に1つだけしか指定しないようにし、他のコンポーネントでは空白のままにしてください。複数の値が存在する場合、Modular Avatarは階層順で最初に指定された初期値を採用します。",
"error.replace_object.null_target": "[MA-0008] 置き換え先が指定されていません",
"error.replace_object.null_target:hint": "Replace Objectは置き換え先のオブジェクトを指定する必要があります。",
"error.replace_object.replacing_replacement": "[MA-0009] 複数のReplace Objectコンポーネントで、同じ置き換え先を指定できません",
"error.replace_object.parent_of_target": "[MA-0010] このオブジェクトの親を置き換え先に指定できません",
"error.singleton": "[MA-0011] {0} はアバターに一個しか存在できません",
"validation.blendshape_sync.no_local_renderer": "[MA-1000] このオブジェクトにはSkinned Mesh Rendererがありません。",
"validation.blendshape_sync.no_local_renderer:hint": "Blendshape Syncは同じGameObject上のSkinned Mesh Rendererに作用します。コンポーネントが正しいオブジェクトに追加されているか確認してください。",
"validation.blendshape_sync.no_local_mesh": "[MA-1001] このオブジェクトにはSkinned Mesh Rendererがありますが、メッシュがありません。",
@ -272,5 +277,15 @@
"ro_sim.effect_group.material.tooltip": "上記の Reactive Component がアクティブな時に設定されるマテリアル",
"ro_sim.effect_group.rule_inverted": "このルールの条件は反転されています",
"ro_sim.effect_group.rule_inverted.tooltip": "このルールは、いずれかの条件が満たされていない場合に適用されます",
"ro_sim.effect_group.conditions": "条件"
"ro_sim.effect_group.conditions": "条件",
"remove-vertex-color.mode": "モード",
"remove-vertex-color.mode.Remove": "頂点カラーを削除する",
"remove-vertex-color.mode.DontRemove": "頂点カラーを削除しない",
"general.vrcsdk-required": "このコンポーネントにはVRCSDKが必要です。",
"sync-param-sequence.platform": "主要プラットホーム",
"sync-param-sequence.platform.tooltip": "このプラットホームでビルドすると、他のプラットホームを合わせるためにパラメーターを記録します。",
"sync-param-sequence.parameters": "共用パラメーターアセット",
"sync-param-sequence.parameters.tooltip": "共用パラメーターがこのアセットに保持されます。アバターデスクリプターに使われるアセットを流用しないでください。",
"sync-param-sequence.create-asset": "新規作成",
"sync-param-sequence.create-asset.tooltip": "新しい共用パラメーターアセットを作成します"
}

View File

@ -251,7 +251,7 @@
"reactive_object.inverse": "反轉條件",
"reactive_object.material-setter.set-to": "將材質設定為:",
"menuitem.misc.add_toggle": "新增開關",
"ro_sim.open_debugger_button": "開啟響應除錯工具",
"ro_sim.open_debugger_button": "開啟 Reaction 除錯工具",
"ro_sim.window.title": "MA 響應除錯工具",
"ro_sim.header.inspecting": "檢視物件",
"ro_sim.header.clear_overrides": "清除所有覆寫",

View File

@ -119,9 +119,11 @@ namespace nadena.dev.modular_avatar.core.editor
internal static VRCExpressionsMenu.Control CloneControl(VRCExpressionsMenu.Control c)
{
var type = c.type != 0 ? c.type : VRCExpressionsMenu.Control.ControlType.Button;
return new VRCExpressionsMenu.Control()
{
type = c.type,
type = type,
name = c.name,
icon = c.icon,
parameter = new VRCExpressionsMenu.Control.Parameter() { name = c.parameter?.name },

View File

@ -65,6 +65,7 @@ namespace nadena.dev.modular_avatar.core.editor
mergeSessions.Clear();
var descriptor = avatarGameObject.GetComponent<VRCAvatarDescriptor>();
if (!descriptor) return;
if (descriptor.baseAnimationLayers != null) InitSessions(descriptor.baseAnimationLayers);
if (descriptor.specialAnimationLayers != null) InitSessions(descriptor.specialAnimationLayers);
@ -246,6 +247,18 @@ namespace nadena.dev.modular_avatar.core.editor
var stateMachineQueue = new Queue<AnimatorStateMachine>();
foreach (var layer in controller.layers)
{
// Special case: A layer with a single state, which contains a blend tree, is ignored for WD analysis.
// This is because WD ON blend trees have different behavior from most WD ON states, and can be safely
// used in a WD OFF animator.
if (layer.stateMachine.states.Length == 1
&& layer.stateMachine.states[0].state.motion is BlendTree
&& layer.stateMachine.stateMachines.Length == 0
)
{
continue;
}
stateMachineQueue.Enqueue(layer.stateMachine);
}

View File

@ -1,4 +1,4 @@
/*
/*
* MIT License
*
* Copyright (c) 2022 bd_
@ -116,6 +116,24 @@ namespace nadena.dev.modular_avatar.core.editor
{
RetainBoneReferences(c as Component);
}
foreach (var smr in avatarGameObject.transform.GetComponentsInChildren<SkinnedMeshRenderer>(true))
{
// If the root bone has been offset, or has a different sign for its scale, we need to retain it.
// see https://github.com/bdunderscore/modular-avatar/pull/1355
// (we avoid retaining otherwise to avoid excess bone transforms)
if (smr.rootBone == null || smr.rootBone.parent == null) continue;
var root = smr.rootBone;
var parent = root.parent;
if ((parent.position - root.position).sqrMagnitude > 0.000001f
|| Vector3.Dot(parent.localScale.normalized, root.localScale.normalized) < 0.9999f)
{
BoneDatabase.RetainMergedBone(smr.rootBone);
}
}
new RetargetMeshes().OnPreprocessAvatar(avatarGameObject, BoneDatabase, PathMappings);
}

View File

@ -1,5 +1,9 @@
using System.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;
using Object = UnityEngine.Object;
namespace nadena.dev.modular_avatar.core.editor
{
@ -50,12 +54,12 @@ namespace nadena.dev.modular_avatar.core.editor
or ModularAvatarMeshSettings.InheritMode.Inherit
or ModularAvatarMeshSettings.InheritMode.DontSet
or ModularAvatarMeshSettings.InheritMode.SetOrInherit), _):
throw new System.InvalidOperationException($"Logic failure: invalid InheritMode: {currentMode}");
throw new InvalidOperationException($"Logic failure: invalid InheritMode: {currentMode}");
case (_, not (ModularAvatarMeshSettings.InheritMode.Set
or ModularAvatarMeshSettings.InheritMode.Inherit
or ModularAvatarMeshSettings.InheritMode.DontSet
or ModularAvatarMeshSettings.InheritMode.SetOrInherit)):
throw new System.ArgumentOutOfRangeException(nameof(srcMode), $"Invalid InheritMode: {srcMode}");
throw new ArgumentOutOfRangeException(nameof(srcMode), $"Invalid InheritMode: {srcMode}");
// If current value is came from Set or DontSet, it should not be changed
case (ModularAvatarMeshSettings.InheritMode.Set, _):
@ -144,9 +148,57 @@ namespace nadena.dev.modular_avatar.core.editor
if (newMesh) context.SaveAsset(newMesh);
}
smr.rootBone = settings.RootBone;
smr.localBounds = settings.Bounds;
var settingsRootBone = settings.RootBone;
settingsRootBone = settingsRootBone == null ? smr.transform : settingsRootBone;
var smrRootBone = smr.rootBone;
smrRootBone = smrRootBone == null ? smr.transform : smrRootBone;
if (IsInverted(smrRootBone) != IsInverted(settingsRootBone))
{
smr.rootBone = GetInvertedRootBone(settingsRootBone);
var bounds = settings.Bounds;
var center = bounds.center;
center.x *= -1;
bounds.center = center;
smr.localBounds = bounds;
}
else
{
smr.rootBone = settings.RootBone;
smr.localBounds = settings.Bounds;
}
}
}
private bool IsInverted(Transform bone)
{
var inverseCount = 0;
var scale = bone.lossyScale;
if (scale.x < 0) inverseCount += 1;
if (scale.y < 0) inverseCount += 1;
if (scale.z < 0) inverseCount += 1;
return (inverseCount % 2) != 0;
}
private Dictionary<Transform, Transform> invertedRootBoneCache = new();
private Transform GetInvertedRootBone(Transform rootBone)
{
if (invertedRootBoneCache.TryGetValue(rootBone, out var cache)) { return cache; }
var invertedRootBone = new GameObject($"{rootBone.gameObject.name}-InvertedRootBone");
EditorUtility.CopySerialized(rootBone, invertedRootBone.transform);
invertedRootBone.transform.parent = rootBone;
var transform = invertedRootBone.transform;
var scale = transform.localScale;
scale.x *= -1;
transform.localScale = scale;
invertedRootBoneCache[rootBone] = transform;
return transform;
}
}
}
}

3
Editor/MiscPreview.meta Normal file
View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: ea61a438a5d54a289c6abbb1e05c56da
timeCreated: 1733085642

View File

@ -0,0 +1,121 @@
#nullable enable
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 RemoveVertexColorPreview : IRenderFilter
{
private static string ToPathString(ComputeContext ctx, Transform t)
{
return string.Join("/", ctx.ObservePath(t).Select(t2 => t2.gameObject.name).Reverse());
}
public ImmutableList<RenderGroup> GetTargetGroups(ComputeContext context)
{
var roots = context.GetAvatarRoots();
var removers = roots
.SelectMany(r => context.GetComponentsInChildren<ModularAvatarRemoveVertexColor>(r, true))
.Select(rvc => (ToPathString(context, rvc.transform),
context.Observe(rvc, r => r.Mode) == ModularAvatarRemoveVertexColor.RemoveMode.Remove))
.OrderBy(pair => pair.Item1)
.ToList();
var targets = roots.SelectMany(
r => context.GetComponentsInChildren<SkinnedMeshRenderer>(r, true)
.Concat(
context.GetComponentsInChildren<MeshFilter>(r, true)
.SelectMany(mf => context.GetComponents<Renderer>(mf.gameObject))
)
);
targets = targets.Where(target =>
{
var stringPath = ToPathString(context, target.transform);
var index = removers.BinarySearch((stringPath, true));
if (index >= 0)
{
// There is a component on this mesh
return true;
}
var priorIndex = ~index - 1;
if (priorIndex < 0) return false; // no match
var (maybeParent, mode) = removers[priorIndex];
if (!stringPath.StartsWith(maybeParent)) return false; // no parent matched
return mode;
});
return targets.Select(RenderGroup.For).ToImmutableList();
}
public Task<IRenderFilterNode> Instantiate(RenderGroup group, IEnumerable<(Renderer, Renderer)> proxyPairs,
ComputeContext context)
{
Dictionary<Mesh, Mesh> conversionMap = new();
foreach (var (_, proxy) in proxyPairs)
{
Component c = proxy;
if (!(c is SkinnedMeshRenderer))
{
c = context.GetComponent<MeshFilter>(proxy.gameObject);
}
if (c == null) continue;
RemoveVertexColorPass.ForceRemove(_ => false, c, conversionMap);
}
return Task.FromResult<IRenderFilterNode>(new Node(conversionMap.Values.FirstOrDefault()));
}
private class Node : IRenderFilterNode
{
private readonly Mesh? _theMesh;
public Node(Mesh? theMesh)
{
_theMesh = theMesh;
}
public Task<IRenderFilterNode> Refresh(IEnumerable<(Renderer, Renderer)> proxyPairs, ComputeContext context,
RenderAspects updatedAspects)
{
if (updatedAspects.HasFlag(RenderAspects.Mesh)) return Task.FromResult<IRenderFilterNode>(null);
if (_theMesh == null) return Task.FromResult<IRenderFilterNode>(null);
return Task.FromResult<IRenderFilterNode>(this);
}
public RenderAspects WhatChanged => RenderAspects.Mesh;
public void Dispose()
{
if (_theMesh != null) Object.DestroyImmediate(_theMesh);
}
public void OnFrame(Renderer original, Renderer proxy)
{
if (_theMesh == null) return;
switch (proxy)
{
case SkinnedMeshRenderer smr: smr.sharedMesh = _theMesh; break;
default:
{
var mf = proxy.GetComponent<MeshFilter>();
if (mf != null) mf.sharedMesh = _theMesh;
break;
}
}
}
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: b05d5c04f86b4924bf8acdd135448463
timeCreated: 1733085648

View File

@ -11,30 +11,62 @@ namespace nadena.dev.modular_avatar.core
{
private static ComputeContext _context;
private static PrefabStage _lastStage;
private static int? _lastStage;
private static int? GetCurrentContentsRootId(out GameObject contentsRoot)
{
contentsRoot = null;
var stage = PrefabStageUtility.GetCurrentPrefabStage();
if (stage == null || stage.prefabContentsRoot == null) return null;
contentsRoot = stage.prefabContentsRoot;
return stage.prefabContentsRoot.GetInstanceID();
}
[InitializeOnLoadMethod]
private static void Init()
{
EditorApplication.delayCall += ProcessObjectReferences;
EditorApplication.update += () =>
{
if (PrefabStageUtility.GetCurrentPrefabStage() != _lastStage) _context?.Invalidate?.Invoke();
var curStage = GetCurrentContentsRootId(out _);
if (curStage != _lastStage)
{
_context?.Invalidate?.Invoke();
}
};
EditorApplication.playModeStateChanged += state =>
{
if (state == PlayModeStateChange.EnteredEditMode)
{
EditorApplication.delayCall += ProcessObjectReferences;
}
};
}
private static void ProcessObjectReferences()
{
_lastStage = PrefabStageUtility.GetCurrentPrefabStage();
if (EditorApplication.isPlayingOrWillChangePlaymode)
{
_context = null;
return;
}
_lastStage = GetCurrentContentsRootId(out var contentsRoot);
AvatarObjectReference.InvalidateAll();
_context = new ComputeContext("ObjectReferenceFixer");
_context.InvokeOnInvalidate<object>(typeof(ObjectReferenceFixer), _ => ProcessObjectReferences());
IEnumerable<IHaveObjReferences> withReferences = _context.GetComponentsByType<IHaveObjReferences>();
if (_lastStage != null)
if (contentsRoot != null)
withReferences =
withReferences.Concat(
_context.GetComponentsInChildren<IHaveObjReferences>(_lastStage.prefabContentsRoot, true)
_context.GetComponentsInChildren<IHaveObjReferences>(contentsRoot, true)
);
foreach (var obj in withReferences)
@ -56,10 +88,26 @@ namespace nadena.dev.modular_avatar.core
foreach (var (targetObject, referencePath, objRef) in references)
{
if (targetObject == null) continue;
_context.ObservePath(targetObject.transform);
var resolvedTarget = objRef.Get(component);
if (objRef.Get(component) == null) continue;
if (targetObject == null)
{
Undo.RecordObject(component, "");
objRef.targetObject = resolvedTarget;
dirty = true;
}
else
{
// Direct object reference always wins in the event of a conflict.
resolvedTarget = targetObject;
}
if (!targetObject.transform.IsChildOf(avatar.transform)) continue;
foreach (var t in _context.ObservePath(resolvedTarget.transform))
{
_context.Observe(t.gameObject, g => g.name);
}
if (!resolvedTarget.transform.IsChildOf(avatar.transform)) continue;
if (objRef.IsConsistent(avatar)) continue;
@ -80,4 +128,4 @@ namespace nadena.dev.modular_avatar.core
}
}
}
}
}

View File

@ -9,6 +9,8 @@ namespace nadena.dev.modular_avatar.core.editor
{
protected override void Execute(ndmf.BuildContext context)
{
if (!context.AvatarDescriptor) return;
var expParams = context.AvatarDescriptor.expressionParameters;
if (expParams != null && context.IsTemporaryAsset(expParams))
{

View File

@ -59,7 +59,8 @@ namespace nadena.dev.modular_avatar.core.editor
public IEnumerable<ProvidedParameter> GetSuppliedParameters(ndmf.BuildContext context = null)
{
return _component.parameters.Select(p =>
return _component.parameters
.Select(p =>
{
AnimatorControllerParameterType paramType;
bool animatorOnly = false;
@ -87,7 +88,7 @@ namespace nadena.dev.modular_avatar.core.editor
_component, PluginDefinition.Instance, paramType)
{
IsAnimatorOnly = animatorOnly,
WantSynced = !p.localOnly,
WantSynced = !p.localOnly && !animatorOnly,
IsHidden = p.internalParameter,
DefaultValue = p.defaultValue
};

View File

@ -2,7 +2,6 @@
using System;
using nadena.dev.modular_avatar.animation;
using nadena.dev.modular_avatar.core.ArmatureAwase;
using nadena.dev.modular_avatar.core.editor.plugin;
using nadena.dev.modular_avatar.editor.ErrorReporting;
using nadena.dev.ndmf;
@ -57,30 +56,41 @@ namespace nadena.dev.modular_avatar.core.editor.plugin
#endif
seq.WithRequiredExtension(typeof(AnimationServicesContext), _s2 =>
{
#if MA_VRCSDK3_AVATARS
seq.Run("Shape Changer", ctx => new ReactiveObjectPass(ctx).Execute())
.PreviewingWith(new ShapeChangerPreview(), new ObjectSwitcherPreview(), new MaterialSetterPreview());
// TODO: We currently run this above MergeArmaturePlugin, because Merge Armature might destroy
// game objects which contain Menu Installers. It'd probably be better however to teach Merge Armature
// to retain those objects? maybe?
seq.Run(MenuInstallPluginPass.Instance);
#endif
seq.Run(MergeArmaturePluginPass.Instance);
seq.Run(BoneProxyPluginPass.Instance);
#if MA_VRCSDK3_AVATARS
seq.Run(VisibleHeadAccessoryPluginPass.Instance);
#endif
seq.Run("World Fixed Object",
ctx => new WorldFixedObjectProcessor().Process(ctx)
);
seq.Run(ReplaceObjectPluginPass.Instance);
#if MA_VRCSDK3_AVATARS
seq.Run(BlendshapeSyncAnimationPluginPass.Instance);
#endif
seq.Run(GameObjectDelayDisablePass.Instance);
#endif
seq.Run(ConstraintConverterPass.Instance);
});
#if MA_VRCSDK3_AVATARS
seq.Run(MenuInstallPluginPass.Instance);
seq.Run(PhysbonesBlockerPluginPass.Instance);
seq.Run("Fixup Expressions Menu", ctx =>
{
var maContext = ctx.Extension<ModularAvatarContext>().BuildContext;
FixupExpressionsMenuPass.FixupExpressionsMenu(maContext);
});
seq.Run(SyncParameterSequencePass.Instance);
#endif
seq.Run(RemoveVertexColorPass.Instance).PreviewingWith(new RemoveVertexColorPreview());
seq.Run(RebindHumanoidAvatarPass.Instance);
seq.Run("Purge ModularAvatar components", ctx =>
{
@ -207,6 +217,7 @@ namespace nadena.dev.modular_avatar.core.editor.plugin
}
}
#if MA_VRCSDK3_AVATARS
class VisibleHeadAccessoryPluginPass : MAPass<VisibleHeadAccessoryPluginPass>
{
protected override void Execute(ndmf.BuildContext context)
@ -214,6 +225,7 @@ namespace nadena.dev.modular_avatar.core.editor.plugin
new VisibleHeadAccessoryProcessor(MAContext(context)).Process();
}
}
#endif
class ReplaceObjectPluginPass : MAPass<ReplaceObjectPluginPass>
{

View File

@ -1,14 +1,14 @@
using System.Collections.Generic;
using UnityEngine;
using System;
using System.Collections.Generic;
using System.Linq;
using Object = UnityEngine.Object;
namespace nadena.dev.modular_avatar.core.editor
{
internal class AnimatedProperty
{
public TargetProp TargetProp { get; }
public string ControlParam { get; set; }
public bool alwaysDeleted;
public object currentState;
// Objects which trigger deletion of this shape key.
@ -25,5 +25,30 @@ namespace nadena.dev.modular_avatar.core.editor
TargetProp = key;
this.currentState = currentState;
}
protected bool Equals(AnimatedProperty other)
{
return Equals(currentState, other.currentState) && actionGroups.SequenceEqual(other.actionGroups) &&
TargetProp.Equals(other.TargetProp);
}
public override bool Equals(object obj)
{
if (obj is null) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != GetType()) return false;
return Equals((AnimatedProperty)obj);
}
public override int GetHashCode()
{
var actionGroupHash = 0;
foreach (var ag in actionGroups)
{
actionGroupHash = HashCode.Combine(actionGroupHash, ag);
}
return HashCode.Combine(currentState, actionGroupHash, TargetProp);
}
}
}

View File

@ -1,11 +1,13 @@
using UnityEngine;
using System;
using UnityEngine;
using Object = UnityEngine.Object;
namespace nadena.dev.modular_avatar.core.editor
{
internal class ControlCondition
{
public string Parameter;
public UnityEngine.Object DebugReference;
public Object DebugReference;
public string DebugName;
public bool IsConstant;
@ -14,5 +16,31 @@ namespace nadena.dev.modular_avatar.core.editor
public bool IsConstantActive => InitiallyActive && IsConstant;
public GameObject ReferenceObject;
protected bool Equals(ControlCondition other)
{
return Parameter == other.Parameter
&& Equals(DebugReference, other.DebugReference)
&& DebugName == other.DebugName
&& IsConstant == other.IsConstant
&& ParameterValueLo.Equals(other.ParameterValueLo)
&& ParameterValueHi.Equals(other.ParameterValueHi)
&& InitialValue.Equals(other.InitialValue)
&& Equals(ReferenceObject, other.ReferenceObject);
}
public override bool Equals(object obj)
{
if (obj is null) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != GetType()) return false;
return Equals((ControlCondition)obj);
}
public override int GetHashCode()
{
return HashCode.Combine(Parameter, DebugReference, DebugName, IsConstant, ParameterValueLo,
ParameterValueHi, InitialValue, ReferenceObject);
}
}
}

View File

@ -1,7 +1,8 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using nadena.dev.modular_avatar.animation;
using UnityEngine;
using Object = UnityEngine.Object;
namespace nadena.dev.modular_avatar.core.editor
{
@ -9,8 +10,8 @@ namespace nadena.dev.modular_avatar.core.editor
{
public ReactionRule(TargetProp key, float value)
: this(key, (object)value) { }
public ReactionRule(TargetProp key, UnityEngine.Object value)
public ReactionRule(TargetProp key, Object value)
: this(key, (object)value) { }
private ReactionRule(TargetProp key, object value)
@ -31,13 +32,15 @@ namespace nadena.dev.modular_avatar.core.editor
public bool InitiallyActive =>
((ControllingConditions.Count == 0) || ControllingConditions.All(c => c.InitiallyActive)) ^ Inverted;
public bool IsDelete;
public bool Inverted;
public bool IsConstant => ControllingConditions.Count == 0 || ControllingConditions.All(c => c.IsConstant);
public bool IsConstantOn => IsConstant && InitiallyActive;
public bool IsConstant => ControllingConditions.Count == 0
|| ControllingConditions.All(c => c.IsConstant)
|| ControllingConditions.Any(c => c.IsConstant && !c.InitiallyActive);
public bool IsConstantActive => IsConstant && InitiallyActive ^ Inverted;
public override string ToString()
{
return $"AGK: {TargetProp}={Value}";
@ -55,9 +58,36 @@ namespace nadena.dev.modular_avatar.core.editor
}
else return false;
if (!ControllingConditions.SequenceEqual(other.ControllingConditions)) return false;
if (IsDelete || other.IsDelete) return false;
return true;
}
protected bool Equals(ReactionRule other)
{
return TargetProp.Equals(other.TargetProp)
&& Equals(Value, other.Value)
&& Equals(ControllingObject, other.ControllingObject)
&& ControllingConditions.SequenceEqual(other.ControllingConditions)
&& Inverted == other.Inverted;
}
public override bool Equals(object obj)
{
if (obj is null) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != GetType()) return false;
return Equals((ReactionRule)obj);
}
public override int GetHashCode()
{
var ccHash = 0;
foreach (var cc in ControllingConditions)
{
ccHash = HashCode.Combine(ccHash, cc);
}
return HashCode.Combine(TargetProp, Value, ControllingObject, ccHash, Inverted);
}
}
}

View File

@ -1,4 +1,6 @@
using System.Collections.Generic;
#if MA_VRCSDK3_AVATARS
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using nadena.dev.ndmf.preview;
using UnityEngine;
@ -38,6 +40,77 @@ namespace nadena.dev.modular_avatar.core.editor
return param;
}
}
private readonly Dictionary<(SkinnedMeshRenderer, string), HashSet<(SkinnedMeshRenderer, string)>>
_blendshapeSyncMappings = new();
private void LocateBlendshapeSyncs(GameObject root)
{
var components = _computeContext.GetComponentsInChildren<ModularAvatarBlendshapeSync>(root, true);
foreach (var bss in components)
{
var localMesh = _computeContext.GetComponent<SkinnedMeshRenderer>(bss.gameObject);
if (localMesh == null) continue;
foreach (var entry in _computeContext.Observe(bss, bss_ => bss_.Bindings.ToImmutableList(),
Enumerable.SequenceEqual))
{
var src = entry.ReferenceMesh.Get(bss);
if (src == null) continue;
var srcMesh = _computeContext.GetComponent<SkinnedMeshRenderer>(src);
var localBlendshape = entry.LocalBlendshape;
if (string.IsNullOrWhiteSpace(localBlendshape))
{
localBlendshape = entry.Blendshape;
}
var srcBinding = (srcMesh, entry.Blendshape);
var dstBinding = (localMesh, localBlendshape);
if (!_blendshapeSyncMappings.TryGetValue(srcBinding, out var dstSet))
{
dstSet = new HashSet<(SkinnedMeshRenderer, string)>();
_blendshapeSyncMappings[srcBinding] = dstSet;
}
dstSet.Add(dstBinding);
}
}
// For recursive blendshape syncs, we need to precompute the full set of affected blendshapes.
foreach (var (src, dsts) in _blendshapeSyncMappings)
{
var visited = new HashSet<(SkinnedMeshRenderer, string)>();
foreach (var item in Visit(src, visited).ToList())
{
dsts.Add(item);
}
}
IEnumerable<(SkinnedMeshRenderer, string)> Visit(
(SkinnedMeshRenderer, string) key,
HashSet<(SkinnedMeshRenderer, string)> visited
)
{
if (!visited.Add(key)) yield break;
if (_blendshapeSyncMappings.TryGetValue(key, out var children))
{
foreach (var child in children)
{
foreach (var item in Visit(child, visited))
{
yield return item;
}
}
}
yield return key;
}
}
private void BuildConditions(Component controllingComponent, ReactionRule rule)
{
@ -124,50 +197,80 @@ namespace nadena.dev.modular_avatar.core.editor
var key = new TargetProp
{
TargetObject = renderer,
PropertyName = "blendShape." + shape.ShapeName,
PropertyName = BlendshapePrefix + shape.ShapeName
};
var currentValue = renderer.GetBlendShapeWeight(shapeId);
var value = shape.ChangeType == ShapeChangeType.Delete ? 100 : shape.Value;
if (!shapeKeys.TryGetValue(key, out var info))
RegisterAction(key, currentValue, value, changer);
if (_blendshapeSyncMappings.TryGetValue((renderer, shape.ShapeName), out var bindings))
{
info = new AnimatedProperty(key, renderer.GetBlendShapeWeight(shapeId));
shapeKeys[key] = info;
// Propagate the new value through any Blendshape Syncs we might have.
// Note that we don't propagate deletes; it's common to e.g. want to delete breasts from the
// base model while retaining outerwear that matches the breast size.
foreach (var binding in bindings)
{
var bindingKey = new TargetProp
{
TargetObject = binding.Item1,
PropertyName = BlendshapePrefix + binding.Item2
};
var bindingRenderer = binding.Item1;
// Add initial state
var agk = new ReactionRule(key, value);
agk.Value = renderer.GetBlendShapeWeight(shapeId);
info.actionGroups.Add(agk);
var bindingMesh = bindingRenderer.sharedMesh;
if (bindingMesh == null) continue;
var bindingShapeIndex = bindingMesh.GetBlendShapeIndex(binding.Item2);
if (bindingShapeIndex < 0) continue;
var bindingInitialState = bindingRenderer.GetBlendShapeWeight(bindingShapeIndex);
RegisterAction(bindingKey, bindingInitialState, value, changer);
}
}
var action = ObjectRule(key, changer, value);
action.Inverted = _computeContext.Observe(changer, c => c.Inverted);
var isCurrentlyActive = changer.gameObject.activeInHierarchy;
if (shape.ChangeType == ShapeChangeType.Delete)
key = new TargetProp
{
action.IsDelete = true;
if (isCurrentlyActive) info.currentState = 100;
TargetObject = renderer,
PropertyName = DeletedShapePrefix + shape.ShapeName
};
info.actionGroups.Add(action); // Never merge
continue;
}
if (changer.gameObject.activeInHierarchy) info.currentState = action.Value;
if (info.actionGroups.Count == 0)
{
info.actionGroups.Add(action);
}
else if (!info.actionGroups[^1].TryMerge(action))
{
info.actionGroups.Add(action);
}
value = shape.ChangeType == ShapeChangeType.Delete ? 1 : 0;
RegisterAction(key, 0, value, changer);
}
}
return shapeKeys;
void RegisterAction(TargetProp key, float currentValue, float value, ModularAvatarShapeChanger changer)
{
if (!shapeKeys.TryGetValue(key, out var info))
{
info = new AnimatedProperty(key, currentValue);
shapeKeys[key] = info;
// Add initial state
var agk = new ReactionRule(key, value);
agk.Value = currentValue;
info.actionGroups.Add(agk);
}
var action = ObjectRule(key, changer, value);
action.Inverted = _computeContext.Observe(changer, c => c.Inverted);
if (changer.gameObject.activeInHierarchy) info.currentState = action.Value;
if (info.actionGroups.Count == 0)
{
info.actionGroups.Add(action);
}
else if (!info.actionGroups[^1].TryMerge(action))
{
info.actionGroups.Add(action);
}
}
}
private void FindMaterialSetters(Dictionary<TargetProp, AnimatedProperty> objectGroups, GameObject root)
@ -244,4 +347,5 @@ namespace nadena.dev.modular_avatar.core.editor
}
}
}
}
}
#endif

View File

@ -1,4 +1,5 @@
using System.Collections.Generic;
#if MA_VRCSDK3_AVATARS
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using nadena.dev.modular_avatar.animation;
@ -18,6 +19,11 @@ namespace nadena.dev.modular_avatar.core.editor
private readonly ndmf.BuildContext _context;
private readonly AnimationServicesContext _asc;
private Dictionary<string, float> _simulationInitialStates;
public const string BlendshapePrefix = "blendShape.";
public const string DeletedShapePrefix = "deletedShape.";
public bool OptimizeShapes = true;
public ImmutableDictionary<string, float> ForcePropertyOverrides { get; set; } = ImmutableDictionary<string, float>.Empty;
@ -58,7 +64,6 @@ namespace nadena.dev.modular_avatar.core.editor
{
public Dictionary<TargetProp, AnimatedProperty> Shapes;
public Dictionary<TargetProp, object> InitialStates;
public HashSet<TargetProp> DeletedShapes;
}
private static PropCache<GameObject, AnalysisResult> _analysisCache;
@ -86,7 +91,6 @@ namespace nadena.dev.modular_avatar.core.editor
/// </summary>
/// <param name="root">The avatar root</param>
/// <param name="initialStates">A dictionary of target property to initial state (float or UnityEngine.Object)</param>
/// <param name="deletedShapes">A hashset of blendshape properties which are always deleted</param>
/// <returns></returns>
public AnalysisResult Analyze(
GameObject root
@ -98,9 +102,10 @@ namespace nadena.dev.modular_avatar.core.editor
{
result.Shapes = new();
result.InitialStates = new();
result.DeletedShapes = new();
return result;
}
LocateBlendshapeSyncs(root);
Dictionary<TargetProp, AnimatedProperty> shapes = FindShapes(root);
FindObjectToggles(shapes, root);
@ -109,7 +114,7 @@ namespace nadena.dev.modular_avatar.core.editor
ApplyInitialStateOverrides(shapes);
AnalyzeConstants(shapes);
ResolveToggleInitialStates(shapes);
PreprocessShapes(shapes, out result.InitialStates, out result.DeletedShapes);
PreprocessShapes(shapes, out result.InitialStates);
result.Shapes = shapes;
return result;
@ -124,7 +129,7 @@ namespace nadena.dev.modular_avatar.core.editor
foreach (var cond in rule.ControllingConditions)
{
var paramName = cond.Parameter;
if (ForcePropertyOverrides.TryGetValue(paramName, out var value))
if (ForcePropertyOverrides?.TryGetValue(paramName, out var value) == true)
{
cond.InitialValue = value;
}
@ -144,7 +149,7 @@ namespace nadena.dev.modular_avatar.core.editor
HashSet<GameObject> toggledObjects = new();
if (asc == null) return;
foreach (var targetProp in shapes.Keys)
if (targetProp is { TargetObject: GameObject go, PropertyName: "m_IsActive" })
toggledObjects.Add(go);
@ -165,7 +170,7 @@ namespace nadena.dev.modular_avatar.core.editor
group.actionGroups.RemoveAll(agk => agk.IsConstant && !agk.InitiallyActive);
// Remove all action groups up until the last one where we're always on
var lastAlwaysOnGroup = group.actionGroups.FindLastIndex(ag => ag.IsConstantOn);
var lastAlwaysOnGroup = group.actionGroups.FindLastIndex(ag => ag.IsConstantActive);
if (lastAlwaysOnGroup > 0)
group.actionGroups.RemoveRange(0, lastAlwaysOnGroup - 1);
}
@ -264,39 +269,26 @@ namespace nadena.dev.modular_avatar.core.editor
}
/// <summary>
/// Determine initial state and deleted shapes for all properties
/// Determine initial state for all properties
/// </summary>
/// <param name="shapes"></param>
/// <param name="initialStates"></param>
/// <param name="deletedShapes"></param>
private void PreprocessShapes(Dictionary<TargetProp, AnimatedProperty> shapes, out Dictionary<TargetProp, object> initialStates, out HashSet<TargetProp> deletedShapes)
private void PreprocessShapes(Dictionary<TargetProp, AnimatedProperty> shapes,
out Dictionary<TargetProp, object> initialStates)
{
// For each shapekey, determine 1) if we can just set an initial state and skip and 2) if we can delete the
// corresponding mesh. If we can't, delete ops are merged into the main list of operations.
initialStates = new Dictionary<TargetProp, object>();
deletedShapes = new HashSet<TargetProp>();
foreach (var (key, info) in shapes.ToList())
{
if (info.actionGroups.Count == 0)
{
// never active control; ignore it entirely
shapes.Remove(key);
if (OptimizeShapes) shapes.Remove(key);
continue;
}
var deletions = info.actionGroups.Where(agk => agk.IsDelete).ToList();
if (deletions.Any(d => d.ControllingConditions.All(c => c.IsConstantActive)))
{
// always deleted
shapes.Remove(key);
deletedShapes.Add(key);
continue;
}
// Move deleted shapes to the end of the list, so they override all Set actions
info.actionGroups = info.actionGroups.Where(agk => !agk.IsDelete).Concat(deletions).ToList();
var initialState = info.actionGroups.Where(agk => agk.InitiallyActive)
.Select(agk => agk.Value)
@ -308,9 +300,10 @@ namespace nadena.dev.modular_avatar.core.editor
// If we're now constant-on, we can skip animation generation
if (info.actionGroups[^1].IsConstant)
{
shapes.Remove(key);
if (OptimizeShapes) shapes.Remove(key);
}
}
}
}
}
}
#endif

View File

@ -1,8 +1,8 @@
#region
#if MA_VRCSDK3_AVATARS
#region
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using nadena.dev.modular_avatar.animation;
using UnityEditor;
@ -34,6 +34,8 @@ namespace nadena.dev.modular_avatar.core.editor
internal void Execute()
{
if (!context.AvatarDescriptor) return;
// Having a WD OFF layer after WD ON layers can break WD. We match the behavior of the existing states,
// and if mixed, use WD ON to maximize compatibility.
_writeDefaults = MergeAnimatorProcessor.ProbeWriteDefaults(FindFxController().animatorController as AnimatorController) ?? true;
@ -42,10 +44,11 @@ namespace nadena.dev.modular_avatar.core.editor
var shapes = analysis.Shapes;
var initialStates = analysis.InitialStates;
var deletedShapes = analysis.DeletedShapes;
GenerateActiveSelfProxies(shapes);
ProcessMeshDeletion(initialStates, shapes);
ProcessInitialStates(initialStates, shapes);
ProcessInitialAnimatorVariables(shapes);
@ -53,8 +56,6 @@ namespace nadena.dev.modular_avatar.core.editor
{
ProcessShapeKey(groups);
}
ProcessMeshDeletion(deletedShapes);
}
private void GenerateActiveSelfProxies(Dictionary<TargetProp, AnimatedProperty> shapes)
@ -85,7 +86,7 @@ namespace nadena.dev.modular_avatar.core.editor
initialValues[condition.Parameter] = condition.InitialValue;
}
}
}
}
private void ProcessInitialStates(Dictionary<TargetProp, object> initialStates,
Dictionary<TargetProp, AnimatedProperty> shapes)
@ -225,30 +226,65 @@ namespace nadena.dev.modular_avatar.core.editor
#region Mesh processing
private void ProcessMeshDeletion(HashSet<TargetProp> deletedKeys)
private void ProcessMeshDeletion(Dictionary<TargetProp, object> initialStates,
Dictionary<TargetProp, AnimatedProperty> shapes)
{
ImmutableDictionary<SkinnedMeshRenderer, List<TargetProp>> renderers = deletedKeys
.GroupBy(
v => (SkinnedMeshRenderer) v.TargetObject
).ToImmutableDictionary(
g => (SkinnedMeshRenderer) g.Key,
g => g.ToList()
);
var renderers = initialStates
.Where(kvp => kvp.Key.PropertyName.StartsWith(ReactiveObjectAnalyzer.DeletedShapePrefix))
.Where(kvp => kvp.Key.TargetObject is SkinnedMeshRenderer)
.Where(kvp => kvp.Value is float f && f > 0.5f)
// Filter any non-constant keys
.Where(kvp =>
{
if (!shapes.ContainsKey(kvp.Key))
{
// Constant value
return true;
}
foreach (var (renderer, infos) in renderers)
var lastGroup = shapes[kvp.Key].actionGroups.LastOrDefault();
return lastGroup?.IsConstantActive == true && lastGroup.Value is float f && f > 0.5f;
})
.GroupBy(kvp => kvp.Key.TargetObject as SkinnedMeshRenderer)
.Select(grouping => (grouping.Key, grouping.Select(
kvp => kvp.Key.PropertyName.Substring(ReactiveObjectAnalyzer.DeletedShapePrefix.Length)
).ToList()))
.ToList();
foreach (var (renderer, shapeNamesToDelete) in renderers)
{
if (renderer == null) continue;
var mesh = renderer.sharedMesh;
if (mesh == null) continue;
renderer.sharedMesh = RemoveBlendShapeFromMesh.RemoveBlendshapes(
mesh,
infos
.Select(i => mesh.GetBlendShapeIndex(i.PropertyName.Substring("blendShape.".Length)))
.Where(k => k >= 0)
.ToList()
);
var shapesToDelete = shapeNamesToDelete
.Select(shape => mesh.GetBlendShapeIndex(shape))
.Where(k => k >= 0)
.ToList();
renderer.sharedMesh = RemoveBlendShapeFromMesh.RemoveBlendshapes(mesh, shapesToDelete);
foreach (var name in shapeNamesToDelete)
{
// Don't need to animate this anymore...!
shapes.Remove(new TargetProp
{
TargetObject = renderer,
PropertyName = ReactiveObjectAnalyzer.BlendshapePrefix + name
});
shapes.Remove(new TargetProp
{
TargetObject = renderer,
PropertyName = ReactiveObjectAnalyzer.DeletedShapePrefix + name
});
initialStates.Remove(new TargetProp
{
TargetObject = renderer,
PropertyName = ReactiveObjectAnalyzer.BlendshapePrefix + name
});
}
}
}
@ -257,10 +293,6 @@ namespace nadena.dev.modular_avatar.core.editor
private void ProcessShapeKey(AnimatedProperty info)
{
// TODO: prune non-animated keys
// Check if this is non-animated and skip most processing if so
if (info.alwaysDeleted || info.actionGroups[^1].IsConstant) return;
var asm = GenerateStateMachine(info);
ApplyController(asm, "MA Responsive: " + info.TargetProp.TargetObject.name);
}
@ -300,8 +332,7 @@ namespace nadena.dev.modular_avatar.core.editor
var transitionBuffer = new List<(AnimatorState, List<AnimatorStateTransition>)>();
var entryTransitions = new List<AnimatorTransition>();
var initialStateTransitionList = new List<AnimatorStateTransition>();
transitionBuffer.Add((initialState, initialStateTransitionList));
transitionBuffer.Add((initialState, new List<AnimatorStateTransition>()));
foreach (var group in info.actionGroups.Skip(lastConstant))
{
@ -321,30 +352,33 @@ namespace nadena.dev.modular_avatar.core.editor
var conditions = GetTransitionConditions(asc, group);
if (!group.Inverted)
foreach (var (st, transitions) in transitionBuffer)
{
var transition = new AnimatorStateTransition
if (!group.Inverted)
{
isExit = true,
hasExitTime = false,
duration = 0,
hasFixedDuration = true,
conditions = (AnimatorCondition[])conditions.Clone()
};
initialStateTransitionList.Add(transition);
}
else
{
foreach (var cond in conditions)
{
initialStateTransitionList.Add(new AnimatorStateTransition
var transition = new AnimatorStateTransition
{
isExit = true,
hasExitTime = false,
duration = 0,
hasFixedDuration = true,
conditions = new[] { InvertCondition(cond) }
});
conditions = (AnimatorCondition[])conditions.Clone()
};
transitions.Add(transition);
}
else
{
foreach (var cond in conditions)
{
transitions.Add(new AnimatorStateTransition
{
isExit = true,
hasExitTime = false,
duration = 0,
hasFixedDuration = true,
conditions = new[] { InvertCondition(cond) }
});
}
}
}
@ -582,3 +616,5 @@ namespace nadena.dev.modular_avatar.core.editor
}
}
}
#endif

View File

@ -1,4 +1,5 @@
using nadena.dev.ndmf;
#if MA_VRCSDK3_AVATARS
using nadena.dev.ndmf;
using UnityEditor.Animations;
using UnityEngine;
@ -14,11 +15,11 @@ namespace nadena.dev.modular_avatar.core.editor
protected override void Execute(ndmf.BuildContext context)
{
var hasShapeChanger = context.AvatarRootObject.GetComponentInChildren<ModularAvatarShapeChanger>() != null;
var hasShapeChanger = context.AvatarRootObject.GetComponentInChildren<ModularAvatarShapeChanger>(true) != null;
var hasObjectSwitcher =
context.AvatarRootObject.GetComponentInChildren<ModularAvatarObjectToggle>() != null;
context.AvatarRootObject.GetComponentInChildren<ModularAvatarObjectToggle>(true) != null;
var hasMaterialSetter =
context.AvatarRootObject.GetComponentInChildren<ModularAvatarMaterialSetter>() != null;
context.AvatarRootObject.GetComponentInChildren<ModularAvatarMaterialSetter>(true) != null;
if (hasShapeChanger || hasObjectSwitcher || hasMaterialSetter)
{
var clip = new AnimationClip();
@ -53,4 +54,5 @@ namespace nadena.dev.modular_avatar.core.editor
}
}
}
}
}
#endif

View File

@ -1,4 +1,5 @@
using System.Collections.Generic;
#if MA_VRCSDK3_AVATARS
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
@ -144,4 +145,5 @@ namespace nadena.dev.modular_avatar.core.editor
}
}
}
}
}
#endif

View File

@ -1,4 +1,5 @@
using System;
#if MA_VRCSDK3_AVATARS
using System;
using System.Collections.Generic;
using nadena.dev.ndmf;
using nadena.dev.ndmf.preview;
@ -70,4 +71,5 @@ namespace nadena.dev.modular_avatar.core.editor
return _context.Observe(mami, _ => mami.isDefault);
}
}
}
}
#endif

View File

@ -1,4 +1,5 @@
using System.Collections.Generic;
#if MA_VRCSDK3_AVATARS
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
@ -104,4 +105,5 @@ namespace nadena.dev.modular_avatar.core.editor
}
}
}
}
}
#endif

View File

@ -1,7 +1,10 @@
using System.Collections.Generic;
#if MA_VRCSDK3_AVATARS
using System.Collections.Generic;
using System.Linq;
using nadena.dev.ndmf;
using UnityEditor.Animations;
using UnityEngine;
using VRC.SDK3.Avatars.Components;
using VRC.SDK3.Avatars.ScriptableObjects;
namespace nadena.dev.modular_avatar.core.editor
@ -141,6 +144,12 @@ namespace nadena.dev.modular_avatar.core.editor
{
mami.Control.value = defaultValue.GetValueOrDefault();
}
else if (p != null && p.valueType != VRCExpressionParameters.ValueType.Int)
{
// For a float or bool value, we don't really have a lot of good choices, so just set it to
// 1
mami.Control.value = 1;
}
else
{
while (usedValues.Contains(nextValue)) nextValue++;
@ -185,6 +194,26 @@ namespace nadena.dev.modular_avatar.core.editor
expParams.parameters = expParams.parameters.Concat(newParameters.Values).ToArray();
}
var mamiWithRC = _mamiByParam.Where(kvp => kvp.Value.Any(
component => component.TryGetComponent<ReactiveComponent>(out _)
)).ToList();
if (mamiWithRC.Count > 0)
{
// This make sures the parameters are correctly merged into the FX layer.
var mergeAnimator = context.AvatarRootObject.AddComponent<ModularAvatarMergeAnimator>();
mergeAnimator.layerType = VRCAvatarDescriptor.AnimLayerType.FX;
mergeAnimator.deleteAttachedAnimator = false;
mergeAnimator.animator = new AnimatorController
{
parameters = mamiWithRC.Select(kvp => new AnimatorControllerParameter
{
name = kvp.Key,
type = AnimatorControllerParameterType.Float,
}).ToArray(),
};
}
}
internal static ControlCondition AssignMenuItemParameter(
@ -205,7 +234,8 @@ namespace nadena.dev.modular_avatar.core.editor
if (simulationInitialStates != null)
{
var isDefault = mami.isDefault;
if (isDefaultOverrides?.TryGetValue(paramName, out var target) == true)
ModularAvatarMenuItem target = null;
if (isDefaultOverrides?.TryGetValue(paramName, out target) == true)
isDefault = ReferenceEquals(mami, target);
if (isDefault)
@ -236,4 +266,5 @@ namespace nadena.dev.modular_avatar.core.editor
};
}
}
}
}
#endif

View File

@ -1,4 +1,5 @@
#region
#if MA_VRCSDK3_AVATARS
#region
using System;
using System.Collections.Generic;
@ -72,8 +73,8 @@ namespace nadena.dev.modular_avatar.core.editor
var analysis = ReactiveObjectAnalyzer.CachedAnalyze(context, avatarRoot);
var shapes = analysis.Shapes;
ImmutableDictionary<SkinnedMeshRenderer, ImmutableList<(int, float)>>.Builder rendererStates =
ImmutableDictionary.CreateBuilder<SkinnedMeshRenderer, ImmutableList<(int, float)>>(
var rendererStates =
ImmutableDictionary.CreateBuilder<SkinnedMeshRenderer, ImmutableDictionary<int, float>>(
);
var avatarRootTransform = avatarRoot.transform;
@ -83,16 +84,29 @@ namespace nadena.dev.modular_avatar.core.editor
var target = prop.TargetProp;
if (target.TargetObject == null || target.TargetObject is not SkinnedMeshRenderer r) continue;
if (!r.transform.IsChildOf(avatarRootTransform)) continue;
if (!target.PropertyName.StartsWith("blendShape.")) continue;
var isDelete = false;
string shapeName = null;
if (target.PropertyName.StartsWith(ReactiveObjectAnalyzer.DeletedShapePrefix))
{
isDelete = true;
shapeName = target.PropertyName.Substring(ReactiveObjectAnalyzer.DeletedShapePrefix.Length);
}
else if (target.PropertyName.StartsWith(ReactiveObjectAnalyzer.BlendshapePrefix))
{
shapeName = target.PropertyName.Substring(ReactiveObjectAnalyzer.BlendshapePrefix.Length);
}
else
{
continue;
}
var mesh = r.sharedMesh;
if (mesh == null) continue;
var shapeName = target.PropertyName.Substring("blendShape.".Length);
if (!rendererStates.TryGetValue(r, out var states))
{
states = ImmutableList<(int, float)>.Empty;
states = ImmutableDictionary<int, float>.Empty;
rendererStates[r] = states;
}
@ -101,16 +115,32 @@ namespace nadena.dev.modular_avatar.core.editor
var activeRule = prop.actionGroups.LastOrDefault(rule => rule.InitiallyActive);
if (activeRule == null || activeRule.Value is not float value) continue;
if (activeRule.ControllingObject == null) continue; // default value is being inherited
value = Math.Clamp(value, 0, 100);
if (activeRule.IsDelete) value = -1;
states = states.Add((index, value));
if (isDelete)
{
if (value < 0.5f) continue;
value = -1;
}
else
{
if (states.ContainsKey(index))
{
// Delete takes precedence over set in preview
continue;
}
value = Math.Clamp(value, 0, 100);
}
states = states.SetItem(index, value);
rendererStates[r] = states;
}
return rendererStates.ToImmutableDictionary();
return rendererStates.ToImmutableDictionary(
kvp => kvp.Key,
kvp => kvp.Value.Select(shapePair => (shapePair.Key, shapePair.Value)
).ToImmutableList());
}
private IEnumerable<RenderGroup> ShapesToGroups(GameObject avatarRoot, ImmutableDictionary<SkinnedMeshRenderer, ImmutableList<(int, float)>> shapes)
@ -265,4 +295,5 @@ namespace nadena.dev.modular_avatar.core.editor
}
}
}
}
}
#endif

View File

@ -1,4 +1,5 @@
using System;
#if MA_VRCSDK3_AVATARS
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
@ -256,7 +257,7 @@ namespace nadena.dev.modular_avatar.core.editor.Simulator
return;
}
_btn_clear.SetEnabled(!PropertyOverrides.Value.IsEmpty || !MenuItemOverrides.Value.IsEmpty);
_btn_clear.SetEnabled(PropertyOverrides.Value?.IsEmpty == false || MenuItemOverrides.Value?.IsEmpty == false);
e_debugInfo.style.display = DisplayStyle.Flex;
@ -264,6 +265,7 @@ namespace nadena.dev.modular_avatar.core.editor.Simulator
_lastComputeContext.InvokeOnInvalidate(this, MaybeRefreshUI);
var analysis = new ReactiveObjectAnalyzer(_lastComputeContext);
analysis.OptimizeShapes = false;
analysis.ForcePropertyOverrides = PropertyOverrides.Value;
analysis.ForceMenuItems = MenuItemOverrides.Value;
var result = analysis.Analyze(avatar.gameObject);
@ -471,7 +473,7 @@ namespace nadena.dev.modular_avatar.core.editor.Simulator
var f_set_inactive = effectGroup.Q<VisualElement>("effect__set-inactive");
var f_value = effectGroup.Q<FloatField>("effect__value");
var f_material = effectGroup.Q<ObjectField>("effect__material");
var f_delete = effectGroup.Q("effect__deleted");
var f_delete = effectGroup.Q<TextField>("effect__deleted");
f_target_component.style.display = DisplayStyle.None;
f_target_component.SetEnabled(false);
@ -504,9 +506,10 @@ namespace nadena.dev.modular_avatar.core.editor.Simulator
f_property.value = targetProp.PropertyName;
f_property.style.display = DisplayStyle.Flex;
if (reactionRule.IsDelete)
if (reactionRule.TargetProp.PropertyName.StartsWith(ReactiveObjectAnalyzer.DeletedShapePrefix))
{
f_delete.style.display = DisplayStyle.Flex;
f_delete.value = reactionRule.Value is > 0.5f ? "DELETE" : "RETAIN";
} else if (reactionRule.Value is float f)
{
f_value.SetValueWithoutNotify(f);
@ -635,4 +638,5 @@ namespace nadena.dev.modular_avatar.core.editor.Simulator
ve_inactive.style.display = activeState ? DisplayStyle.None : DisplayStyle.Flex;
}
}
}
}
#endif

View File

@ -1,5 +1,7 @@
using nadena.dev.modular_avatar.core.editor;
#if MA_VRCSDK3_AVATARS
using nadena.dev.modular_avatar.core.editor.Simulator;
#endif
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
@ -42,11 +44,13 @@ namespace nadena.dev.modular_avatar.core.editor
private void OpenDebugger()
{
#if MA_VRCSDK3_AVATARS
GameObject target = Selection.activeGameObject;
if (ReferenceObject is Component c) target = c.gameObject;
else if (ReferenceObject is GameObject go) target = go;
ROSimulator.OpenDebugger(target);
#endif
}
}
}

View File

@ -1,4 +1,6 @@
using nadena.dev.modular_avatar.core.editor.Simulator;
#if MA_VRCSDK3_AVATARS
using System;
using nadena.dev.modular_avatar.core.editor.Simulator;
using UnityEditor;
using UnityEngine.UIElements;
@ -74,4 +76,5 @@ namespace nadena.dev.modular_avatar.core.editor
}
}
}
}
}
#endif

View File

@ -0,0 +1,93 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using nadena.dev.ndmf;
using UnityEditor;
using UnityEngine;
using UnityEngine.Rendering;
using Object = UnityEngine.Object;
namespace nadena.dev.modular_avatar.core.editor
{
internal class RemoveVertexColorPass : Pass<RemoveVertexColorPass>
{
protected override void Execute(ndmf.BuildContext context)
{
var removers = context.AvatarRootTransform.GetComponentsInChildren<ModularAvatarRemoveVertexColor>(true)!;
Dictionary<Mesh, Mesh> conversionMap = new();
foreach (var remover in removers)
{
foreach (var smr in remover!.GetComponentsInChildren<SkinnedMeshRenderer>(true))
{
TryRemove(context.IsTemporaryAsset, smr, conversionMap);
}
foreach (var mf in remover.GetComponentsInChildren<MeshFilter>(true))
{
TryRemove(context.IsTemporaryAsset, mf, conversionMap);
}
}
}
private const string PropPath = "m_Mesh";
private static void TryRemove(
Func<Mesh, bool> isTempAsset,
Component c,
Dictionary<Mesh, Mesh> conversionMap
)
{
var nearestRemover = c.GetComponentInParent<ModularAvatarRemoveVertexColor>()!;
if (nearestRemover.Mode != ModularAvatarRemoveVertexColor.RemoveMode.Remove) return;
ForceRemove(isTempAsset, c, conversionMap);
}
internal static void ForceRemove(Func<Mesh, bool> isTempAsset, Component c,
Dictionary<Mesh, Mesh> conversionMap)
{
var obj = new SerializedObject(c);
var prop = obj.FindProperty("m_Mesh");
if (prop == null)
{
throw new Exception("Property not found: " + PropPath);
}
var mesh = prop.objectReferenceValue as Mesh;
if (mesh == null)
{
return;
}
var originalMesh = mesh;
if (conversionMap.TryGetValue(mesh, out var converted))
{
prop.objectReferenceValue = converted;
obj.ApplyModifiedPropertiesWithoutUndo();
return;
}
if (mesh.GetVertexAttributes().All(va => va.attribute != VertexAttribute.Color))
{
// no-op
return;
}
if (!isTempAsset(mesh))
{
mesh = Object.Instantiate(mesh);
prop.objectReferenceValue = mesh;
obj.ApplyModifiedPropertiesWithoutUndo();
}
mesh.colors = null;
conversionMap[originalMesh] = mesh;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: a227da6f9f1548c3867b1ed113f28e9d
timeCreated: 1733008734

View File

@ -136,6 +136,7 @@ namespace nadena.dev.modular_avatar.core.editor
if (ResolvedParameter.syncType == ParameterSyncType.NotSynced)
{
ResolvedParameter.syncType = info.ResolvedParameter.syncType;
ResolvedParameter.localOnly = info.ResolvedParameter.localOnly;
} else if (ResolvedParameter.syncType != info.ResolvedParameter.syncType && info.ResolvedParameter.syncType != ParameterSyncType.NotSynced)
{
TypeConflict = true;
@ -159,6 +160,8 @@ namespace nadena.dev.modular_avatar.core.editor
public void OnPreprocessAvatar(GameObject avatar, BuildContext context)
{
if (!context.AvatarDescriptor) return;
_context = context;
var syncParams = WalkTree(avatar);
@ -732,6 +735,7 @@ namespace nadena.dev.modular_avatar.core.editor
ParameterConfig parameterConfig = param;
parameterConfig.nameOrPrefix = remapTo;
parameterConfig.remapTo = remapTo;
parameterConfig.localOnly = parameterConfig.localOnly || param.syncType == ParameterSyncType.NotSynced;
var info = new ParameterInfo()
{
ResolvedParameter = parameterConfig,

View File

@ -344,7 +344,7 @@ namespace nadena.dev.modular_avatar.core.editor
if (proxy == null) return;
var curParent = proxy.transform.parent ?? original.transform.parent;
if (_finalBonesMap.TryGetValue(curParent, out var newRoot))
if (curParent != null && _finalBonesMap.TryGetValue(curParent, out var newRoot))
{
// We need to remember this proxy so we can avoid destroying it when we destroy VirtualAvatarRoot
// in Dispose

View File

@ -8,6 +8,7 @@ using UnityEditor;
using UnityEngine;
using Object = UnityEngine.Object;
using static nadena.dev.modular_avatar.core.editor.Localization;
using System;
#endregion
@ -145,25 +146,57 @@ namespace nadena.dev.modular_avatar.core.editor
out var avatarRoot, out var avatarHips, out var outfitHips)
) return;
Undo.SetCurrentGroupName("Setup Outfit");
var avatarArmature = avatarHips.transform.parent;
var outfitArmature = outfitHips.transform.parent;
if (outfitArmature.GetComponent<ModularAvatarMergeArmature>() == null)
var merge = outfitArmature.GetComponent<ModularAvatarMergeArmature>();
if (merge == null)
{
merge = Undo.AddComponent<ModularAvatarMergeArmature>(outfitArmature.gameObject);
} else {
Undo.RecordObject(merge, "");
}
if (merge.mergeTarget == null || merge.mergeTargetObject == null)
{
var merge = Undo.AddComponent<ModularAvatarMergeArmature>(outfitArmature.gameObject);
merge.mergeTarget = new AvatarObjectReference();
merge.mergeTarget.referencePath = RuntimeUtil.RelativePath(avatarRoot, avatarArmature.gameObject);
merge.LockMode = ArmatureLockMode.BaseToMerge;
}
if (string.IsNullOrEmpty(merge.prefix) && string.IsNullOrEmpty(merge.suffix))
{
merge.InferPrefixSuffix();
}
List<Transform> subRoots = new List<Transform>();
HeuristicBoneMapper.RenameBonesByHeuristic(merge, skipped: subRoots);
PrefabUtility.RecordPrefabInstancePropertyModifications(merge);
// If the outfit has an UpperChest bone but the avatar doesn't, add an additional MergeArmature to
// help with this
foreach (var subRoot in subRoots)
var outfitAnimator = outfitRoot.GetComponent<Animator>();
var outfitHumanoidBones = GetOutfitHumanoidBones(outfitRoot.transform, outfitAnimator);
var avatarAnimator = avatarRoot.GetComponent<Animator>();
List<Transform> subRoots = new List<Transform>();
HeuristicBoneMapper.RenameBonesByHeuristic(merge, skipped: subRoots, outfitHumanoidBones: outfitHumanoidBones, avatarAnimator: avatarAnimator);
// If the outfit has an UpperChest bone but the avatar doesn't, add an additional MergeArmature to
// help with this
foreach (var subRoot in subRoots)
{
var subConfig = subRoot.GetComponent<ModularAvatarMergeArmature>();
var subConfigMangleNames = false;
if (subConfig == null)
{
subConfig = Undo.AddComponent<ModularAvatarMergeArmature>(subRoot.gameObject);
}
else
{
Undo.RecordObject(subConfig, "");
subConfigMangleNames = subConfig.mangleNames;
}
if (subConfig.mergeTarget == null || subConfig.mergeTargetObject == null)
{
var subConfig = Undo.AddComponent<ModularAvatarMergeArmature>(subRoot.gameObject);
var parentTransform = subConfig.transform.parent;
var parentConfig = parentTransform.GetComponentInParent<ModularAvatarMergeArmature>();
var parentMapping = parentConfig.MapBone(parentTransform);
@ -174,34 +207,51 @@ namespace nadena.dev.modular_avatar.core.editor
subConfig.LockMode = ArmatureLockMode.BaseToMerge;
subConfig.prefix = merge.prefix;
subConfig.suffix = merge.suffix;
subConfig.mangleNames = false;
subConfig.mangleNames = subConfigMangleNames;
PrefabUtility.RecordPrefabInstancePropertyModifications(subConfig);
}
}
var avatarRootMatchingArmature = avatarRoot.transform.Find(outfitArmature.gameObject.name);
if (merge.prefix == "" && merge.suffix == "" && avatarRootMatchingArmature != null)
{
// We have an armature whose names exactly match the root armature - this can cause some serious
// confusion in Unity's humanoid armature matching system. Fortunately, we can avoid this by
// renaming a bone close to the root; this will ensure the number of matching bones is small, and
// Unity's heuristics (apparently) will choose the base avatar's armature as the "true" armature.
outfitArmature.name += ".1";
var avatarRootMatchingArmature = avatarRoot.transform.Find(outfitArmature.gameObject.name);
if (merge.prefix == "" && merge.suffix == "" && avatarRootMatchingArmature != null)
{
// We have an armature whose names exactly match the root armature - this can cause some serious
// confusion in Unity's humanoid armature matching system. Fortunately, we can avoid this by
// renaming a bone close to the root; this will ensure the number of matching bones is small, and
// Unity's heuristics (apparently) will choose the base avatar's armature as the "true" armature.
outfitArmature.name += ".1";
// Also make sure to refresh the avatar's animator humanoid bone cache.
var avatarAnimator = avatarRoot.GetComponent<Animator>();
var humanDescription = avatarAnimator.avatar;
avatarAnimator.avatar = null;
// ReSharper disable once Unity.InefficientPropertyAccess
avatarAnimator.avatar = humanDescription;
}
// Also make sure to refresh the avatar's animator humanoid bone cache.
var humanDescription = avatarAnimator.avatar;
avatarAnimator.avatar = null;
// ReSharper disable once Unity.InefficientPropertyAccess
avatarAnimator.avatar = humanDescription;
}
FixAPose(avatarRoot, outfitArmature);
if (outfitRoot != null
&& outfitRoot.GetComponent<ModularAvatarMeshSettings>() == null
&& outfitRoot.GetComponentInParent<ModularAvatarMeshSettings>() == null)
var meshSettings = outfitRoot.GetComponent<ModularAvatarMeshSettings>();
var mSInheritProbeAnchor = ModularAvatarMeshSettings.InheritMode.SetOrInherit;
var mSInheritBounds = ModularAvatarMeshSettings.InheritMode.SetOrInherit;
if (outfitRoot != null)
{
if (meshSettings == null)
{
meshSettings = Undo.AddComponent<ModularAvatarMeshSettings>(outfitRoot.gameObject);
}
else
{
Undo.RecordObject(meshSettings, "");
mSInheritProbeAnchor = meshSettings.InheritProbeAnchor;
mSInheritBounds = meshSettings.InheritBounds;
}
}
if (meshSettings != null
&& (meshSettings.ProbeAnchor == null || meshSettings.ProbeAnchor.Get(meshSettings) == null
|| meshSettings.RootBone == null || meshSettings.RootBone.Get(meshSettings) == null))
{
var meshSettings = Undo.AddComponent<ModularAvatarMeshSettings>(outfitRoot.gameObject);
Transform rootBone = null, probeAnchor = null;
Bounds bounds = ModularAvatarMeshSettings.DEFAULT_BOUNDS;
@ -217,8 +267,8 @@ namespace nadena.dev.modular_avatar.core.editor
rootBone = avatarRoot.transform;
}
meshSettings.InheritProbeAnchor = ModularAvatarMeshSettings.InheritMode.SetOrInherit;
meshSettings.InheritBounds = ModularAvatarMeshSettings.InheritMode.SetOrInherit;
meshSettings.InheritProbeAnchor = mSInheritProbeAnchor;
meshSettings.InheritBounds = mSInheritBounds;
meshSettings.ProbeAnchor = new AvatarObjectReference();
meshSettings.ProbeAnchor.referencePath = RuntimeUtil.RelativePath(avatarRoot, probeAnchor.gameObject);
@ -226,10 +276,43 @@ namespace nadena.dev.modular_avatar.core.editor
meshSettings.RootBone = new AvatarObjectReference();
meshSettings.RootBone.referencePath = RuntimeUtil.RelativePath(avatarRoot, rootBone.gameObject);
meshSettings.Bounds = bounds;
PrefabUtility.RecordPrefabInstancePropertyModifications(meshSettings);
}
}
private static void FixAPose(GameObject avatarRoot, Transform outfitArmature)
internal static Dictionary<Transform, HumanBodyBones> GetOutfitHumanoidBones(Transform outfitRoot, Animator outfitAnimator)
{
if (outfitAnimator != null)
{
var hipsCheck = outfitAnimator.isHuman ? outfitAnimator.GetBoneTransform(HumanBodyBones.Hips) : null;
if (hipsCheck != null && hipsCheck.parent == outfitRoot)
{
// Sometimes broken rigs can have the hips as a direct child of the root, instead of having
// an intermediate Armature object. We do not currently support this kind of rig, and so we'll
// assume the outfit's humanoid rig is broken and move on to heuristic matching.
outfitAnimator = null;
} else if (hipsCheck == null) {
outfitAnimator = null;
}
}
Dictionary<Transform, HumanBodyBones> outfitHumanoidBones = null;
if (outfitAnimator != null)
{
outfitHumanoidBones = new Dictionary<Transform, HumanBodyBones>();
foreach (HumanBodyBones boneIndex in Enum.GetValues(typeof(HumanBodyBones)))
{
var bone = boneIndex != HumanBodyBones.LastBone ? outfitAnimator.GetBoneTransform(boneIndex) : null;
if (bone == null) continue;
outfitHumanoidBones[bone] = boneIndex;
}
}
return outfitHumanoidBones;
}
internal static void FixAPose(GameObject avatarRoot, Transform outfitArmature, bool strictMode = true)
{
var mergeArmature = outfitArmature.GetComponent<ModularAvatarMergeArmature>();
if (mergeArmature == null) return;
@ -249,7 +332,7 @@ namespace nadena.dev.modular_avatar.core.editor
{
var lowerArm = (HumanBodyBones)((int)arm + 2);
// check if the rotation of the arm differs, but distances and origin point are the same
// check if the rotation of the arm differs(, but distances and origin point are the same when strictMode)
var avatarArm = rootAnimator.GetBoneTransform(arm);
var outfitArm = avatarToOutfit(avatarArm);
@ -259,22 +342,27 @@ namespace nadena.dev.modular_avatar.core.editor
if (outfitArm == null) return;
if (outfitLowerArm == null) return;
if ((avatarArm.position - outfitArm.position).magnitude > 0.001f) return;
if (strictMode)
{
if ((avatarArm.position - outfitArm.position).magnitude > 0.001f) return;
// check relative distance to lower arm as well
var avatarArmLength = (avatarLowerArm.position - avatarArm.position).magnitude;
var outfitArmLength = (outfitLowerArm.position - outfitArm.position).magnitude;
// check relative distance to lower arm as well
var avatarArmLength = (avatarLowerArm.position - avatarArm.position).magnitude;
var outfitArmLength = (outfitLowerArm.position - outfitArm.position).magnitude;
if (Mathf.Abs(avatarArmLength - outfitArmLength) > 0.001f) return;
if (Mathf.Abs(avatarArmLength - outfitArmLength) > 0.001f) return;
} else {
if (Vector3.Dot((outfitLowerArm.position - outfitArm.position).normalized, (avatarLowerArm.position - avatarArm.position).normalized) > 0.999f) return;
}
// Rotate the outfit arm to ensure these two points match.
// Rotate the outfit arm to ensure these two bone orientations match.
Undo.RecordObject(outfitArm, "Convert A/T Pose");
var relRot = Quaternion.FromToRotation(
outfitLowerArm.position - outfitArm.position,
avatarLowerArm.position - avatarArm.position
);
outfitArm.rotation = relRot * outfitArm.rotation;
PrefabUtility.RecordPrefabInstancePropertyModifications(outfitArm);
EditorUtility.SetDirty(outfitArm);
}
Transform avatarToOutfit(Transform avBone)
@ -490,6 +578,7 @@ namespace nadena.dev.modular_avatar.core.editor
}
var hipsCandidates = new List<string>();
var hipsExtraCandidateRoots = new List<Transform>();
if (outfitHips == null)
{
@ -498,6 +587,23 @@ namespace nadena.dev.modular_avatar.core.editor
foreach (Transform child in outfitRoot.transform)
{
foreach (Transform tempHip in child)
{
if (tempHip.name.Contains(avatarHips.name))
{
outfitHips = tempHip.gameObject;
// Prefer the first hips we find
break;
}
hipsExtraCandidateRoots.Add(tempHip);
}
if (outfitHips != null) return true; // found an exact match, bail outgit
}
// Sometimes, Hips is in deeper place(like root -> Armature -> Armature 1 -> Hips).
foreach (Transform extraCandidateRoot in hipsExtraCandidateRoots)
{
foreach (Transform tempHip in extraCandidateRoot)
{
if (tempHip.name.Contains(avatarHips.name))
{
@ -511,6 +617,7 @@ namespace nadena.dev.modular_avatar.core.editor
}
hipsCandidates.Add(avatarHips.name);
hipsExtraCandidateRoots = new List<Transform>();
// If that doesn't work out, we'll check for heuristic bone mapper mappings.
foreach (var hbm in HeuristicBoneMapper.BoneToNameMap[HumanBodyBones.Hips])
@ -531,6 +638,25 @@ namespace nadena.dev.modular_avatar.core.editor
{
outfitHips = tempHip.gameObject;
}
hipsExtraCandidateRoots.Add(tempHip);
}
}
}
if (outfitHips == null)
{
// Sometimes, Hips is in deeper place(like root -> Armature -> Armature 1 -> Hips).
foreach (Transform extraCandidateRoot in hipsExtraCandidateRoots)
{
foreach (Transform tempHip in extraCandidateRoot)
{
foreach (var candidate in hipsCandidates)
{
if (HeuristicBoneMapper.NormalizeName(tempHip.name).Contains(candidate))
{
outfitHips = tempHip.gameObject;
}
}
}
}
}

View File

@ -0,0 +1,118 @@
#nullable enable
using System;
using System.Collections.Specialized;
using System.Linq;
using nadena.dev.modular_avatar.editor.ErrorReporting;
using nadena.dev.ndmf;
using UnityEditor;
using VRC.SDK3.Avatars.ScriptableObjects;
using static nadena.dev.modular_avatar.core.ModularAvatarSyncParameterSequence;
using Object = UnityEngine.Object;
namespace nadena.dev.modular_avatar.core.editor
{
public class SyncParameterSequencePass : Pass<SyncParameterSequencePass>
{
private static Platform? CurrentPlatform
{
get
{
switch (EditorUserBuildSettings.activeBuildTarget)
{
case BuildTarget.Android: return Platform.Android;
case BuildTarget.iOS: return Platform.iOS;
case BuildTarget.StandaloneWindows64: return Platform.PC;
case BuildTarget.StandaloneLinux64: return Platform.PC; // for CI
default: return null;
}
}
}
protected override void Execute(ndmf.BuildContext context)
{
ExecuteStatic(context);
}
internal static void ExecuteStatic(ndmf.BuildContext context)
{
var avDesc = context.AvatarDescriptor;
var components = context.AvatarRootObject.GetComponentsInChildren<ModularAvatarSyncParameterSequence>(true);
if (components.Length == 0) return;
if (components.Length > 1)
{
BuildReport.LogFatal("error.singleton", "Sync Parameter Sequence", components.Cast<object>().ToArray());
return;
}
var syncComponent = components[0];
if (syncComponent.Parameters == null) return;
if (avDesc.expressionParameters == null) return;
var avatarParams = avDesc.expressionParameters;
if (!context.IsTemporaryAsset(avatarParams))
{
avatarParams = Object.Instantiate(avatarParams);
avDesc.expressionParameters = avatarParams;
}
if (syncComponent.Parameters.parameters == null)
{
syncComponent.Parameters.parameters = Array.Empty<VRCExpressionParameters.Parameter>();
EditorUtility.SetDirty(syncComponent.Parameters);
}
// If we're on the primary platform, add in any unknown parameters, and prune if we exceed the limit.
if (CurrentPlatform != null && CurrentPlatform == syncComponent.PrimaryPlatform)
{
var registered = new OrderedDictionary();
foreach (var param in syncComponent.Parameters.parameters)
{
if (param == null) continue;
if (!param.networkSynced) continue;
registered[param.name] = param;
}
foreach (var param in avatarParams.parameters)
{
if (param == null) continue;
if (!param.networkSynced) continue;
registered[param.name] = param;
}
syncComponent.Parameters.parameters = registered.Values.Cast<VRCExpressionParameters.Parameter>().ToArray();
if (!syncComponent.Parameters.IsWithinBudget())
{
var knownParams = avatarParams.parameters.Where(p => p != null).Select(p => p.name).ToHashSet();
syncComponent.Parameters.parameters = syncComponent.Parameters.parameters.Where(
p => p != null && knownParams.Contains(p.name)
).ToArray();
}
EditorUtility.SetDirty(syncComponent.Parameters);
}
// Now copy back...
OrderedDictionary finalParams = new();
foreach (var param in syncComponent.Parameters.parameters)
{
if (param == null) continue;
if (!param.networkSynced) continue;
finalParams[param.name] = param;
}
foreach (var param in avatarParams.parameters)
{
if (param == null) continue;
finalParams[param.name] = param;
}
avatarParams.parameters = finalParams.Values.Cast<VRCExpressionParameters.Parameter>().ToArray();
EditorUtility.SetDirty(avatarParams);
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 756425df8aeb4926afceda71bedffa40
timeCreated: 1733011801

View File

@ -1,4 +1,5 @@
#region
#if MA_VRCSDK3_AVATARS
#region
using System;
using System.Collections.Generic;
@ -251,3 +252,5 @@ namespace nadena.dev.modular_avatar.core.editor
}
}
}
#endif

View File

@ -33,4 +33,4 @@ For more information, check out the [documentation](https://m-a.nadena.dev).
* 部分的なアニメーターを親に統合することで、様々のギミックの実装を簡単にします。
* 他にもいろいろ!
詳しくは[ドキュメンテーションページにご参照ください](https://modular-avatar.nadena.dev/ja/).
詳しくは[ドキュメンテーションページをご覧ください](https://modular-avatar.nadena.dev/ja/).

View File

@ -19,7 +19,7 @@ namespace nadena.dev.modular_avatar.core
/// initially inactive in the scene (which can have high overhead if the user has a lot of inactive avatars in the
/// scene).
/// </summary>
[AddComponentMenu("")]
[AddComponentMenu("/")]
[ExecuteInEditMode]
[DefaultExecutionOrder(-9998)]
public class Activator : MonoBehaviour, IEditorOnly
@ -30,7 +30,7 @@ namespace nadena.dev.modular_avatar.core
}
}
[AddComponentMenu("")]
[AddComponentMenu("/")]
[ExecuteInEditMode]
[DefaultExecutionOrder(-9997)]
public class AvatarActivator : MonoBehaviour, IEditorOnly

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: abec4f397dc74b2f9bba6f71b5e702f3
timeCreated: 1732395066

View File

@ -0,0 +1,647 @@
using System;
using System.Collections.Generic;
using Unity.Burst;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Jobs;
using UnityEngine;
using UnityEngine.Jobs;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace nadena.dev.modular_avatar.core.armature_lock
{
internal class MaMoveIndependentlyManager
{
internal static MaMoveIndependentlyManager Instance { get; } = new();
private MaMoveIndependentlyManager()
{
_nativeMemoryManager = new NativeMemoryManager();
_vpState = _nativeMemoryManager.CreateArray<TransformState>();
_tpState = _nativeMemoryManager.CreateArray<TransformState>();
_targetState = _nativeMemoryManager.CreateArray<TransformState>();
_mappingStates = _nativeMemoryManager.CreateArray<MappingState>();
_errorFlags = _nativeMemoryManager.CreateArray<bool>();
_enabled = _nativeMemoryManager.CreateArray<bool>();
_sceneRootParent = _nativeMemoryManager.CreateArray<bool>();
_falseArray = _nativeMemoryManager.CreateArray<bool>();
_anyError = new NativeArray<bool>(1, Allocator.Persistent);
_anyDirty = new NativeArray<bool>(1, Allocator.Persistent);
_nativeMemoryManager.OnSegmentMove += MoveTransforms;
#if UNITY_EDITOR
AssemblyReloadEvents.beforeAssemblyReload += OnDomainUnload;
#endif
}
private void OnDomainUnload()
{
Dispose();
}
private void Dispose()
{
_lastJob.Complete();
#if UNITY_EDITOR
AssemblyReloadEvents.beforeAssemblyReload -= OnDomainUnload;
#endif
if (_virtualParents.isCreated) DeferDestroy.DeferDestroyObj(_virtualParents);
if (_trueParents.isCreated) DeferDestroy.DeferDestroyObj(_trueParents);
if (_targets.isCreated) DeferDestroy.DeferDestroyObj(_targets);
_nativeMemoryManager.Dispose();
if (_anyError.IsCreated) _anyError.Dispose();
if (_anyDirty.IsCreated) _anyDirty.Dispose();
}
private const float PosEpsilon = 0.0000001f;
private const float RotEpsilon = 0.0000001f;
private const float ScaleEpsilon = 0.0000001f;
// Our basic strategy is to identify all children of MoveIndependently objects, and to find the first parent
// that is not a member of the same MoveIndependently group. We then compute the local transform of the child
// relative to that parent, and keep it constant (unless the true local transform of the child changes).
//
// If an active MAMoveIndep is a child of another MAMoveIndep, we consider it to be ungrouped (even if it's
// named in the parent).
private readonly NativeMemoryManager _nativeMemoryManager;
private Transform[] _virtualParentsT;
private Transform[] _trueParentsT;
private Transform[] _targetsT;
private TransformAccessArray _virtualParents;
private TransformAccessArray _trueParents;
private TransformAccessArray _targets;
private bool _transformAccessDirty;
private readonly NativeArrayRef<TransformState> _vpState;
private readonly NativeArrayRef<TransformState> _tpState;
private readonly NativeArrayRef<TransformState> _targetState;
private readonly NativeArrayRef<MappingState> _mappingStates;
private readonly NativeArrayRef<bool> _errorFlags;
private NativeArray<bool> _anyError, _anyDirty;
private readonly NativeArrayRef<bool> _enabled;
private readonly NativeArrayRef<bool> _sceneRootParent, _falseArray;
private readonly Dictionary<int, State> _slotToState = new();
private struct MappingState
{
// Our last observed local transform, relative to our actual parent transform
public TransformState TrueLocal;
// Our last observed local transform, relative to our virtual parent transform
public Matrix4x4 VirtualLocal;
// The position of our parent relative to our virtual parent
public Matrix4x4 TrueLocalToVirtualLocal;
public bool RequestWriteback, CacheValid;
}
private class State
{
public MAMoveIndependently MoveIndep;
public ISegment Segment;
}
private readonly Dictionary<MAMoveIndependently, State> _moveIndeps = new();
private JobHandle _lastJob;
private bool _isRegistered;
private int _maxComputeDepth;
private bool UpdateRegistered
{
get => _isRegistered;
set
{
if (value == _isRegistered) return;
if (value)
{
UpdateLoopController.OnMoveIndependentlyUpdate += Update;
}
else
{
UpdateLoopController.OnMoveIndependentlyUpdate -= Update;
}
_isRegistered = value;
}
}
private void EnsureTransformCapacity(int targetLength)
{
if (_virtualParentsT == null)
{
_virtualParentsT = new Transform[targetLength];
_trueParentsT = new Transform[targetLength];
_targetsT = new Transform[targetLength];
return;
}
if (targetLength <= _virtualParentsT.Length) return;
var newCapacity = Mathf.Max(_virtualParentsT.Length * 2, targetLength);
Array.Resize(ref _virtualParentsT, newCapacity);
Array.Resize(ref _trueParentsT, newCapacity);
Array.Resize(ref _targetsT, newCapacity);
}
private void MoveTransforms(int oldoffset, int newoffset, int length)
{
Array.Copy(_virtualParentsT, oldoffset, _virtualParentsT, newoffset, length);
Array.Copy(_trueParentsT, oldoffset, _trueParentsT, newoffset, length);
Array.Copy(_targetsT, oldoffset, _targetsT, newoffset, length);
_transformAccessDirty = true;
}
private void UpdateTransformAccess()
{
if (!_transformAccessDirty) return;
UpdateTransformAccess(ref _virtualParents, _virtualParentsT);
UpdateTransformAccess(ref _trueParents, _trueParentsT);
UpdateTransformAccess(ref _targets, _targetsT);
_transformAccessDirty = false;
}
private void UpdateTransformAccess(ref TransformAccessArray arr, Transform[] t)
{
if (!arr.isCreated || arr.length != t.Length)
{
if (arr.isCreated) arr.Dispose();
arr = new TransformAccessArray(t);
}
else
{
arr.SetTransforms(t);
}
}
private void Update()
{
_lastJob.Complete();
UpdateTransformAccess();
_anyError[0] = false;
_anyDirty[0] = false;
var clearErrors = new JClearErrorFlags
{
ErrorFlags = _errorFlags
};
var clearErrorsHandle = clearErrors.Schedule(_errorFlags.Length, 16);
var readVp = new JReadTransforms
{
States = _vpState,
Enabled = _enabled,
ErrorFlags = _errorFlags,
SceneRootParent = _sceneRootParent
};
var readTp = new JReadTransforms
{
States = _tpState,
Enabled = _enabled,
ErrorFlags = _errorFlags,
SceneRootParent = _falseArray
};
var readTarget = new JReadTransforms
{
States = _targetState,
Enabled = _enabled,
ErrorFlags = _errorFlags,
SceneRootParent = _falseArray
};
var readVpHandle = readVp.Schedule(_virtualParents, clearErrorsHandle);
var clearVpHandle = new JClearRootTransforms
{
States = _vpState,
SceneRootParent = _sceneRootParent
}.Schedule(_vpState.Length, 16, readVpHandle);
var readTpHandle = readTp.Schedule(_trueParents, clearErrorsHandle);
var readTargetHandle = readTarget.Schedule(_targets, clearErrorsHandle);
var readHandle = JobHandle.CombineDependencies(clearVpHandle, readTpHandle, readTargetHandle);
var compute = new JCompute
{
VpState = _vpState,
TpState = _tpState,
TargetState = _targetState,
States = _mappingStates,
AnyDirty = _anyDirty,
AnyError = _anyError,
ErrorFlags = _errorFlags,
Enabled = _enabled
};
var computeHandle = compute.Schedule(_mappingStates.Length, 16, readHandle);
_lastJob = computeHandle;
computeHandle.Complete();
List<Transform> prefabRecord = null;
if (_anyDirty[0])
{
prefabRecord = new List<Transform>();
for (var i = 0; i < _mappingStates.Length; i++)
{
if (_mappingStates[i].RequestWriteback)
{
#if UNITY_EDITOR
Undo.RecordObject(_targets[i], "Move Independently");
#endif
prefabRecord.Add(_targets[i]);
}
}
}
var writeback = new JWriteback
{
States = _mappingStates,
Errors = _errorFlags,
Enabled = _enabled,
AnyError = _anyError
};
var writebackHandle = writeback.Schedule(_targets, computeHandle);
_lastJob = writebackHandle;
writebackHandle.Complete();
if (prefabRecord != null)
{
foreach (var transform in prefabRecord)
{
#if UNITY_EDITOR
PrefabUtility.RecordPrefabInstancePropertyModifications(transform);
#endif
}
}
if (_anyError[0])
{
List<MAMoveIndependently> reactivate = new();
for (var i = 0; i < _mappingStates.Length; i++)
{
if (_errorFlags[i] && _slotToState.TryGetValue(i, out var state))
{
Deactivate(state);
reactivate.Add(state.MoveIndep);
}
}
foreach (var moveIndep in reactivate)
{
if (moveIndep != null) Activate(moveIndep);
}
}
}
internal void Activate(MAMoveIndependently moveIndep)
{
if (!_anyDirty.IsCreated) return; // domain reload timing issues
if (_moveIndeps.TryGetValue(moveIndep, out var state)) Deactivate(state);
HashSet<Transform> groupedTransforms = new();
groupedTransforms.Add(moveIndep.transform);
RegisterGroupedTransforms(moveIndep, groupedTransforms);
List<MAMoveIndependently> toReregister = new();
foreach (var t in groupedTransforms)
{
// If we have a direct child MAMI, we need it to change its virtual parent, so trigger a reregister
// on it.
if (t.TryGetComponent<MAMoveIndependently>(out var mami) && mami != moveIndep)
{
toReregister.Add(mami);
}
}
var ptr = moveIndep.transform.parent;
while (ptr != null)
{
var parentMoveIndep = ptr.GetComponentInParent<MAMoveIndependently>();
if (parentMoveIndep == null) break;
RegisterGroupedTransforms(parentMoveIndep, groupedTransforms);
ptr = parentMoveIndep.transform.parent;
}
// Compute leaf transforms
List<Transform> leafTransforms = new();
Walk(moveIndep.transform);
var segment = _nativeMemoryManager.Allocate(leafTransforms.Count);
EnsureTransformCapacity(segment.Offset + segment.Length);
_transformAccessDirty = true;
var virtualParent = moveIndep.transform.parent;
while (virtualParent != null && groupedTransforms.Contains(virtualParent))
virtualParent = virtualParent.parent;
for (var i = 0; i < leafTransforms.Count; i++)
{
var j = i + segment.Offset;
_mappingStates[j] = new MappingState
{
CacheValid = false
};
_virtualParentsT[j] = virtualParent;
_trueParentsT[j] = leafTransforms[i].parent;
_targetsT[j] = leafTransforms[i];
_enabled[j] = true;
_sceneRootParent[j] = virtualParent == null;
_slotToState[j] = state;
}
_moveIndeps[moveIndep] = new State
{
MoveIndep = moveIndep,
Segment = segment
};
UpdateRegistered = true;
foreach (var mami in toReregister)
{
if (mami != null) Activate(mami);
}
void Walk(Transform t)
{
foreach (Transform child in t)
{
if (groupedTransforms.Contains(child))
{
Walk(child);
continue;
}
leafTransforms.Add(child);
}
}
}
private void RegisterGroupedTransforms(MAMoveIndependently moveIndep, HashSet<Transform> groupedTransforms)
{
var candidates = new HashSet<GameObject>(moveIndep.GroupedBones);
candidates.Add(moveIndep.gameObject);
Walk(moveIndep.transform);
void Walk(Transform t)
{
if (!candidates.Contains(t.gameObject)) return;
groupedTransforms.Add(t);
foreach (Transform child in t)
{
if (child.TryGetComponent<MAMoveIndependently>(out _)) continue;
Walk(child);
}
}
}
internal void Deactivate(MAMoveIndependently moveIndep)
{
if (_moveIndeps.TryGetValue(moveIndep, out var state)) Deactivate(state);
}
private void Deactivate(State state)
{
if (!_anyDirty.IsCreated) return; // domain reload timing issues
for (var i = 0; i < state.Segment.Length; i++)
{
var j = i + state.Segment.Offset;
_enabled[j] = false;
_virtualParents[j] = null;
_trueParents[j] = null;
_targets[j] = null;
_slotToState.Remove(j);
}
_nativeMemoryManager.Free(state.Segment);
_moveIndeps.Remove(state.MoveIndep);
if (_moveIndeps.Count == 0) UpdateRegistered = false;
}
[BurstCompile]
private static bool MatDiffers(Matrix4x4 a, Matrix4x4 b)
{
var aPos = a.GetColumn(3);
var bPos = b.GetColumn(3);
if ((aPos - bPos).sqrMagnitude > PosEpsilon) return true;
var aRot = a.rotation;
var bRot = b.rotation;
if (Quaternion.Angle(aRot, bRot) > RotEpsilon) return true;
var aScale = a.lossyScale;
var bScale = b.lossyScale;
return (aScale - bScale).sqrMagnitude > ScaleEpsilon;
}
private struct JClearErrorFlags : IJobParallelFor
{
[WriteOnly] public NativeArray<bool> ErrorFlags;
public void Execute(int index)
{
ErrorFlags[index] = false;
}
}
// For some reason checking SceneRootParent in JReadTransforms was ignored...?
// Maybe IJobParallelForTransform doesn't execute on null transforms.
private struct JClearRootTransforms : IJobParallelFor
{
[WriteOnly] public NativeArray<TransformState> States;
[ReadOnly] public NativeArray<bool> SceneRootParent;
public void Execute(int index)
{
if (SceneRootParent[index])
{
States[index] = new TransformState
{
localToWorldMatrix = Matrix4x4.identity,
localRotation = Quaternion.identity,
localScale = Vector3.one,
localPosition = Vector3.zero
};
}
}
}
private struct JReadTransforms : IJobParallelForTransform
{
[WriteOnly] public NativeArray<TransformState> States;
[ReadOnly] public NativeArray<bool> Enabled;
[ReadOnly] public NativeArray<bool> SceneRootParent;
[NativeDisableContainerSafetyRestriction] [WriteOnly]
public NativeArray<bool> ErrorFlags;
[BurstCompile]
public void Execute(int index, TransformAccess transform)
{
if (!Enabled[index]) return;
if (SceneRootParent[index]) return;
if (!transform.isValid)
{
ErrorFlags[index] = true;
return;
}
States[index] = new TransformState
{
localToWorldMatrix = transform.localToWorldMatrix,
localRotation = transform.localRotation,
localScale = transform.localScale,
localPosition = transform.localPosition
};
}
}
private struct JCompute : IJobParallelFor
{
[ReadOnly] public NativeArray<TransformState> VpState, TpState, TargetState;
[WriteOnly] [NativeDisableContainerSafetyRestriction]
public NativeArray<bool> AnyDirty;
[WriteOnly] [NativeDisableContainerSafetyRestriction]
public NativeArray<bool> AnyError;
public NativeArray<MappingState> States;
public NativeArray<bool> ErrorFlags;
[ReadOnly] public NativeArray<bool> Enabled;
[BurstCompile]
public void Execute(int index)
{
if (!Enabled[index]) return;
var state = States[index];
var vp = VpState[index];
var tp = TpState[index];
var target = TargetState[index];
if (ErrorFlags[index])
{
AnyError[0] = true;
return;
}
// First, compute the virtual parent transform - we'll need it in any case.
var trueLocalToVirtualLocal = vp.worldToLocalMatrix * tp.localToWorldMatrix;
state.RequestWriteback = false;
if (TransformState.Differs(target, state.TrueLocal) || !state.CacheValid)
{
// Our local position changed, so don't try to make any corrections; just remember the new values.
state.CacheValid = true;
state.TrueLocal = target;
state.TrueLocalToVirtualLocal = trueLocalToVirtualLocal;
state.VirtualLocal = trueLocalToVirtualLocal * Matrix4x4.TRS(
state.TrueLocal.localPosition,
state.TrueLocal.localRotation,
state.TrueLocal.localScale
);
}
else if (MatDiffers(trueLocalToVirtualLocal, state.TrueLocalToVirtualLocal))
{
// Our local position didn't change, but our virtual parent did, so we need to correct.
// To do this, we take our _old_ virtual local transform, and use it to transform our old true local
// position into virtual local space; we then go from _current_ virtual local space to true local.
var virtualLocalToTrueLocal = trueLocalToVirtualLocal.inverse;
var trueLocal = virtualLocalToTrueLocal * state.VirtualLocal;
state.TrueLocal = new TransformState
{
localPosition = trueLocal.GetColumn(3),
localRotation = trueLocal.rotation,
localScale = trueLocal.lossyScale
};
state.TrueLocalToVirtualLocal = trueLocalToVirtualLocal;
state.RequestWriteback = true;
AnyDirty[0] = true;
}
States[index] = state;
}
}
private struct JWriteback : IJobParallelForTransform
{
[ReadOnly] public NativeArray<MappingState> States;
[ReadOnly] public NativeArray<bool> Errors;
[ReadOnly] public NativeArray<bool> Enabled;
[NativeDisableContainerSafetyRestriction] [WriteOnly]
public NativeArray<bool> AnyError;
[BurstCompile]
public void Execute(int index, TransformAccess transform)
{
var state = States[index];
if (!Enabled[index] || Errors[index] || !state.RequestWriteback) return;
if (!transform.isValid)
{
Errors[index] = true;
AnyError[0] = true;
return;
}
var pos = state.TrueLocal.localPosition;
var rot = state.TrueLocal.localRotation;
var scale = state.TrueLocal.localScale;
transform.localPosition = pos;
transform.localRotation = rot;
transform.localScale = scale;
}
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 154891b009044835b43580e745f50a9e
timeCreated: 1732394861

View File

@ -4,7 +4,6 @@ using System;
using System.Collections.Generic;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using UnityEngine;
#endregion
@ -15,12 +14,19 @@ namespace nadena.dev.modular_avatar.core.armature_lock
internal NativeArray<T> Array;
public static implicit operator NativeArray<T>(NativeArrayRef<T> arrayRef) => arrayRef.Array;
public int Length => Array.Length;
public void Dispose()
{
Array.Dispose();
}
public T this[int key]
{
get => Array[key];
set => Array[key] = value;
}
public void Resize(int n)
{
if (Array.Length == n) return;
@ -143,6 +149,11 @@ namespace nadena.dev.modular_avatar.core.armature_lock
// We perform trial creations of segments (and then immediately free them if they exceed the bounds of the
// array). As such, we clamp the length, rather than throwing an exception.
length = Math.Min(length, InUseMask.Array.Length - offset);
if (length < 0)
{
throw new ArgumentException("negative length");
}
unsafe
{

View File

@ -29,6 +29,11 @@ namespace nadena.dev.modular_avatar.core
EditorApplication.hierarchyChanged += () => HIERARCHY_CHANGED_SEQ += 1;
}
#endif
internal static void InvalidateAll()
{
HIERARCHY_CHANGED_SEQ++;
}
public AvatarObjectReference Clone()
{

View File

@ -1,13 +1,14 @@
using System;
using System.Collections.Generic;
using nadena.dev.modular_avatar.core.armature_lock;
using UnityEditor;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
#if MA_VRCSDK3_AVATARS
using VRC.SDKBase;
#endif
namespace nadena.dev.modular_avatar.core.ArmatureAwase
namespace nadena.dev.modular_avatar.core
{
[ExecuteInEditMode]
[AddComponentMenu("Modular Avatar/MA Move Independently")]
@ -22,227 +23,35 @@ namespace nadena.dev.modular_avatar.core.ArmatureAwase
public GameObject[] GroupedBones
{
get => m_groupedBones.Clone() as GameObject[];
get => m_groupedBones?.Clone() as GameObject[] ?? Array.Empty<GameObject>();
set
{
m_groupedBones = value.Clone() as GameObject[];
OnValidate();
MaMoveIndependentlyManager.Instance.Activate(this);
}
}
struct ChildState
{
internal Vector3 childLocalPos;
internal Quaternion childLocalRot;
internal Vector3 childLocalScale;
// The child world position, recorded when we first initialized (or after unexpected child movement)
internal Matrix4x4 childToRoot;
}
private Dictionary<Transform, ChildState> _children = new Dictionary<Transform, ChildState>();
private HashSet<Transform> _excluded = new HashSet<Transform>();
void Awake()
{
hideFlags = HideFlags.DontSave;
}
// We need to reparent the TRS values of the children from our prior frame state to the current frame state.
// This is done by computing the world affine matrix for the child in the prior frame, then converting to
// a local affine matrix in the current frame.
private void OnValidate()
{
hideFlags = HideFlags.DontSave;
_excluded = new HashSet<Transform>();
if (m_groupedBones == null)
{
m_groupedBones = Array.Empty<GameObject>();
}
foreach (var grouped in m_groupedBones)
{
if (grouped != null)
{
_excluded.Add(grouped.transform);
}
}
_priorFramePos = transform.localPosition;
_priorFrameRot = transform.localRotation;
_priorFrameScale = transform.localScale;
_children.Clear();
CheckChildren();
}
HashSet<Transform> _observed = new HashSet<Transform>();
private void CheckChildren()
{
_observed.Clear();
CheckChildren(transform);
foreach (var obj in m_groupedBones)
{
CheckChildren(obj.transform);
}
// Remove any children that are no longer children
var toRemove = new List<Transform>();
foreach (var child in _children)
{
if (child.Key == null || !_observed.Contains(child.Key))
{
toRemove.Add(child.Key);
}
}
foreach (var child in toRemove)
{
_children.Remove(child);
}
}
private Matrix4x4 ParentTransformMatrix(Transform parent)
{
Matrix4x4 transform = Matrix4x4.TRS(
parent.localPosition,
parent.localRotation,
parent.localScale
);
if (_excluded.Contains(parent))
{
transform = ParentTransformMatrix(parent.parent) * transform;
}
return transform;
}
private void CheckChildren(Transform parent)
{
Matrix4x4 parentToRoot = ParentTransformMatrix(parent);
Matrix4x4 rootToParent = parentToRoot.inverse;
foreach (Transform child in parent)
{
if (_excluded.Contains(child)) continue;
_observed.Add(child);
var localPosition = child.localPosition;
var localRotation = child.localRotation;
var localScale = child.localScale;
if (!ArmatureLockController.MovedThisFrame && _children.TryGetValue(child, out var state))
{
var deltaPos = localPosition - state.childLocalPos;
var deltaRot = Quaternion.Angle(localRotation, state.childLocalRot);
var deltaScale = (localScale - state.childLocalScale).sqrMagnitude;
if (deltaPos.magnitude > EPSILON || deltaRot > EPSILON || deltaScale > EPSILON)
{
// The child object was moved in between parent updates; reconstruct its childToRoot to correct
// for this.
var oldChildTRS = Matrix4x4.TRS(
state.childLocalPos,
state.childLocalRot,
state.childLocalScale
);
var newChildTRS = Matrix4x4.TRS(
localPosition,
localRotation,
localScale
);
state.childToRoot = state.childToRoot * oldChildTRS.inverse * newChildTRS;
}
Matrix4x4 childNewLocal = rootToParent * state.childToRoot;
var newPosition = childNewLocal.MultiplyPoint(Vector3.zero);
var newRotation = childNewLocal.rotation;
var newScale = childNewLocal.lossyScale;
#if UNITY_EDITOR
Undo.RecordObject(child, Undo.GetCurrentGroupName());
#endif
child.localPosition = newPosition;
child.localRotation = newRotation;
child.localScale = newScale;
state.childLocalPos = child.localPosition;
state.childLocalRot = child.localRotation;
state.childLocalScale = child.localScale;
_children[child] = state;
continue;
}
Matrix4x4 childTRS = Matrix4x4.TRS(localPosition, localRotation, localScale);
state = new ChildState()
if (!PrefabUtility.IsPartOfPrefabAsset(this))
{
EditorApplication.delayCall += () =>
{
childLocalPos = localPosition,
childLocalRot = localRotation,
childLocalScale = localScale,
childToRoot = parentToRoot * childTRS,
if (this != null) MaMoveIndependentlyManager.Instance.Activate(this);
};
_children[child] = state;
}
#endif
}
private void OnEnable()
{
UpdateLoopController.OnMoveIndependentlyUpdate += OnUpdate;
MaMoveIndependentlyManager.Instance.Activate(this);
}
private void OnDisable()
{
UpdateLoopController.OnMoveIndependentlyUpdate -= OnUpdate;
}
private Vector3 _priorFramePos, _priorFrameScale;
private Quaternion _priorFrameRot;
void OnUpdate()
{
if (this == null)
{
UpdateLoopController.OnMoveIndependentlyUpdate -= OnUpdate;
return;
}
var pos = transform.localPosition;
var rot = transform.localRotation;
var scale = transform.localScale;
var deltaPos = transform.parent.localToWorldMatrix.MultiplyVector(pos - _priorFramePos);
var deltaRot = Quaternion.Angle(rot, _priorFrameRot);
var deltaScaleX = Mathf.Abs((scale - _priorFrameScale).x) / _priorFrameScale.x;
var deltaScaleY = Mathf.Abs((scale - _priorFrameScale).y) / _priorFrameScale.y;
var deltaScaleZ = Mathf.Abs((scale - _priorFrameScale).z) / _priorFrameScale.z;
if (float.IsNaN(deltaScaleX) || float.IsInfinity(deltaScaleX)) deltaScaleX = 1;
if (float.IsNaN(deltaScaleY) || float.IsInfinity(deltaScaleY)) deltaScaleY = 1;
if (float.IsNaN(deltaScaleZ) || float.IsInfinity(deltaScaleZ)) deltaScaleZ = 1;
float maxDeltaScale = Mathf.Max(deltaScaleX, Mathf.Max(deltaScaleY, deltaScaleZ));
if (deltaPos.magnitude > EPSILON || deltaRot > EPSILON || maxDeltaScale > 0.001)
{
CheckChildren();
_priorFramePos = pos;
_priorFrameRot = rot;
_priorFrameScale = scale;
}
MaMoveIndependentlyManager.Instance.Deactivate(this);
}
}
}

View File

@ -1,5 +1,7 @@
#if MA_VRCSDK3_AVATARS
using System;
using JetBrains.Annotations;
using UnityEngine;
using VRC.SDK3.Avatars.ScriptableObjects;
@ -12,6 +14,16 @@ namespace nadena.dev.modular_avatar.core
public VRCExpressionsMenu menuToAppend;
public VRCExpressionsMenu installTargetMenu;
internal static Action<ModularAvatarMenuInstaller> _openSelectMenu = _ => { };
/// <summary>
/// Opens the "Select Menu" window, as if the user had clicked this button in the inspector.
/// </summary>
[PublicAPI]
public void OpenSelectMenu()
{
_openSelectMenu(this);
}
// ReSharper disable once Unity.RedundantEventFunction
void Start()

View File

@ -6,7 +6,7 @@ namespace nadena.dev.modular_avatar.core
#if MA_VRCSDK3_AVATARS
[AddComponentMenu("Modular Avatar/MA Convert Constraints")]
#else
[AddComponentMenu("")]
[AddComponentMenu("/")]
#endif
[HelpURL("https://modular-avatar.nadena.dev/docs/reference/convert-constraints?lang=auto")]
public class ModularAvatarConvertConstraints : AvatarTagComponent

View File

@ -26,6 +26,8 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using nadena.dev.modular_avatar.core.armature_lock;
using UnityEngine;
using UnityEngine.Serialization;
@ -49,6 +51,10 @@ namespace nadena.dev.modular_avatar.core
[HelpURL("https://modular-avatar.nadena.dev/docs/reference/merge-armature?lang=auto")]
public class ModularAvatarMergeArmature : AvatarTagComponent, IHaveObjReferences
{
// Injected by HeuristicBoneMapper
internal static Func<string, string> NormalizeBoneName;
internal static ImmutableHashSet<string> AllBoneNames;
public AvatarObjectReference mergeTarget = new AvatarObjectReference();
public GameObject mergeTargetObject => mergeTarget.Get(this);
@ -61,6 +67,9 @@ namespace nadena.dev.modular_avatar.core
public bool mangleNames = true;
// Inserted from HeuristicBoneMapper(Editor Assembly) with InitializeOnLoadMethod
// We use raw `boneNamePatterns` instead of `BoneToNameMap` because BoneToNameMap requires matching with normalized bone name, but normalizing makes raw prefix/suffix unavailable.
internal static string[][] boneNamePatterns;
private ArmatureLockController _lockController;
internal Transform MapBone(Transform bone)
@ -200,6 +209,66 @@ namespace nadena.dev.modular_avatar.core
}
}
class PSCandidate
{
public string prefix, suffix;
public int matches;
public PSCandidate CountMatches(ModularAvatarMergeArmature merger)
{
var target = merger.mergeTarget.Get(merger).transform;
var source = merger.transform;
var oldPrefix = merger.prefix;
var oldSuffix = merger.suffix;
try
{
merger.prefix = prefix;
merger.suffix = suffix;
matches = merger.GetBonesForLock().Count;
return this;
}
finally
{
merger.prefix = oldPrefix;
merger.suffix = oldSuffix;
}
}
/// <summary>
/// Counts the number of children which take the form prefix // heuristic bone name // suffix
/// </summary>
/// <returns></returns>
public PSCandidate CountHeuristicMatches(Transform root)
{
int count = 1;
Walk(root);
matches = count;
return this;
void Walk(Transform t)
{
foreach (Transform child in t)
{
if (child.name.StartsWith(prefix) && child.name.EndsWith(suffix))
{
var boneName = child.name.Substring(prefix.Length, child.name.Length - prefix.Length - suffix.Length);
boneName = NormalizeBoneName(boneName);
if (AllBoneNames.Contains(boneName))
{
count++;
Walk(child);
}
}
}
}
}
}
public void InferPrefixSuffix()
{
// We only infer if targeting the armature (below the Hips bone)
@ -212,18 +281,65 @@ namespace nadena.dev.modular_avatar.core
// We also require that the attached object has exactly one child (presumably the hips)
if (transform.childCount != 1) return;
List<PSCandidate> candidates = new();
// always consider the current configuration
candidates.Add(new PSCandidate() {prefix = prefix, suffix = suffix}.CountMatches(this));
// Infer the prefix and suffix by comparing the names of the mergeTargetObject's hips with the child of the
// GameObject we're attached to.
var baseName = hips.name;
var mergeName = transform.GetChild(0).name;
var mergeHips = transform.GetChild(0);
var mergeName = mergeHips.name;
var prefixLength = mergeName.IndexOf(baseName, StringComparison.InvariantCulture);
if (prefixLength < 0) return;
// Classic substring match
{
var prefixLength = mergeName.IndexOf(baseName, StringComparison.InvariantCulture);
if (prefixLength >= 0)
{
var suffixLength = mergeName.Length - prefixLength - baseName.Length;
var suffixLength = mergeName.Length - prefixLength - baseName.Length;
candidates.Add(new PSCandidate()
{
prefix = mergeName.Substring(0, prefixLength),
suffix = mergeName.Substring(mergeName.Length - suffixLength)
}.CountMatches(this));
}
}
prefix = mergeName.Substring(0, prefixLength);
suffix = mergeName.Substring(mergeName.Length - suffixLength);
// Heuristic match - try to see if we get a better prefix/suffix pattern if we allow for fuzzy-matching of
// bone names. Since our goal is to minimize unnecessary renaming (and potentially failing matches), we do
// this only if the number of heuristic matches is more than twice the number of matches from the static
// pattern above, as using this will force most bones to be renamed.
foreach (var hipNameCandidate in
boneNamePatterns[(int)HumanBodyBones.Hips].OrderByDescending(p => p.Length))
{
var prefixLength = mergeName.IndexOf(hipNameCandidate, StringComparison.InvariantCultureIgnoreCase);
if (prefixLength < 0) continue;
var suffixLength = mergeName.Length - prefixLength - hipNameCandidate.Length;
var prefix = mergeName.Substring(0, prefixLength);
var suffix = mergeName.Substring(mergeName.Length - suffixLength);
var candidate = new PSCandidate
{
prefix = prefix,
suffix = suffix
}.CountHeuristicMatches(mergeHips);
candidate.matches = (candidate.matches + 1) / 2;
candidates.Add(candidate);
break;
}
// Select which candidate to use
var selected = candidates.OrderByDescending(c => c.matches).FirstOrDefault();
if (selected != null && selected.matches > 0)
{
prefix = selected.prefix;
suffix = selected.suffix;
}
if (prefix == "J_Bip_C_")
{
@ -242,4 +358,4 @@ namespace nadena.dev.modular_avatar.core
if (mergeTarget != null) yield return mergeTarget;
}
}
}
}

View File

@ -0,0 +1,31 @@
using System;
using JetBrains.Annotations;
using UnityEngine;
using VRC.SDK3.Avatars.ScriptableObjects;
namespace nadena.dev.modular_avatar.core
{
[AddComponentMenu("Modular Avatar/MA Sync Parameter Sequence")]
[DisallowMultipleComponent]
[HelpURL("https://modular-avatar.nadena.dev/docs/reference/sync-parameter-sequence?lang=auto")]
[PublicAPI]
public class ModularAvatarSyncParameterSequence : AvatarTagComponent
{
[Serializable]
[PublicAPI]
public enum Platform
{
PC,
Android,
iOS
}
public Platform PrimaryPlatform = Platform.Android;
#if MA_VRCSDK3_AVATARS
public VRCExpressionParameters Parameters;
#else
// preserve settings on non-VRC platforms at least
public UnityEngine.Object Parameters;
#endif
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 934543afe4744213b5621aa13a67e3b4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: a8edd5bd1a0a64a40aa99cc09fb5f198, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -38,7 +38,7 @@ namespace nadena.dev.modular_avatar.core
}
[AddComponentMenu("Modular Avatar/MA Material Setter")]
[HelpURL("https://modular-avatar.nadena.dev/docs/reference/material-setter?lang=auto")]
[HelpURL("https://modular-avatar.nadena.dev/docs/reference/reaction/material-setter?lang=auto")]
public class ModularAvatarMaterialSetter : ReactiveComponent, IHaveObjReferences
{
[SerializeField] private List<MaterialSwitchObject> m_objects = new();

View File

@ -21,7 +21,7 @@ namespace nadena.dev.modular_avatar.core
}
[AddComponentMenu("Modular Avatar/MA Object Toggle")]
[HelpURL("https://modular-avatar.nadena.dev/docs/reference/object-toggle?lang=auto")]
[HelpURL("https://modular-avatar.nadena.dev/docs/reference/reaction/object-toggle?lang=auto")]
public class ModularAvatarObjectToggle : ReactiveComponent, IHaveObjReferences
{
[SerializeField] private List<ToggledObject> m_objects = new();

View File

@ -57,14 +57,9 @@ namespace nadena.dev.modular_avatar.core
}
[AddComponentMenu("Modular Avatar/MA Shape Changer")]
[HelpURL("https://modular-avatar.nadena.dev/docs/reference/shape-changer?lang=auto")]
[HelpURL("https://modular-avatar.nadena.dev/docs/reference/reaction/shape-changer?lang=auto")]
public class ModularAvatarShapeChanger : ReactiveComponent, IHaveObjReferences
{
// Migration field to help with 1.10-beta series avatar data. Since this was never in a released version of MA,
// this migration support will be removed in 1.10.0.
[SerializeField] [FormerlySerializedAs("targetRenderer")] [HideInInspector]
private AvatarObjectReference m_targetRenderer = new();
[SerializeField] [FormerlySerializedAs("Shapes")]
private List<ChangedShape> m_shapes = new();
@ -82,40 +77,6 @@ namespace nadena.dev.modular_avatar.core
}
}
private void OnEnable()
{
MigrateTargetRenderer();
}
protected override void OnValidate()
{
base.OnValidate();
MigrateTargetRenderer();
}
// Migrate early versions of MASC (from Modular Avatar 1.10.0-beta.4 or earlier) to the new format, where the
// target renderer is stored separately for each shape.
// This logic will be removed in 1.10.0.
private void MigrateTargetRenderer()
{
// Note: This method runs in the context of OnValidate, and therefore cannot touch any other unity objects.
if (!string.IsNullOrEmpty(m_targetRenderer.referencePath) || m_targetRenderer.targetObject != null)
{
foreach (var shape in m_shapes)
{
if (shape.Object == null) shape.Object = new AvatarObjectReference();
if (string.IsNullOrEmpty(shape.Object.referencePath) && shape.Object.targetObject == null)
{
shape.Object.referencePath = m_targetRenderer.referencePath;
shape.Object.targetObject = m_targetRenderer.targetObject;
}
}
m_targetRenderer.referencePath = null;
m_targetRenderer.targetObject = null;
}
}
public IEnumerable<AvatarObjectReference> GetObjectReferences()
{
foreach (var shape in m_shapes)

View File

@ -0,0 +1,23 @@
using System;
using JetBrains.Annotations;
using UnityEngine;
namespace nadena.dev.modular_avatar.core
{
[AddComponentMenu("Modular Avatar/MA Remove Vertex Color")]
[DisallowMultipleComponent]
[HelpURL("https://modular-avatar.nadena.dev/docs/reference/remove-vertex-color?lang=auto")]
[PublicAPI]
public class ModularAvatarRemoveVertexColor : AvatarTagComponent
{
[Serializable]
[PublicAPI]
public enum RemoveMode
{
Remove,
DontRemove
}
public RemoveMode Mode = RemoveMode.Remove;
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: dc5f8bfae24244aeaedcd6c2bb7264f9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: a8edd5bd1a0a64a40aa99cc09fb5f198, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,4 +1,6 @@
using nadena.dev.modular_avatar.core.editor;
#if MA_VRCSDK3_AVATARS
using nadena.dev.modular_avatar.core.editor;
using NUnit.Framework;
using UnityEditor;
using UnityEngine;
@ -36,4 +38,6 @@ namespace modular_avatar_tests
Assert.AreEqual("x", curves[0].propertyName);
}
}
}
}
#endif

View File

@ -1,4 +1,6 @@
using System;
#if MA_VRCSDK3_AVATARS
using System;
using System.Collections.Generic;
using System.Linq;
using nadena.dev.ndmf;
@ -158,4 +160,6 @@ namespace modular_avatar_tests
Assert.IsFalse(state.transformMaskElements.Any(e => e.Item1 == "Armature/Hips/UpperLeg.R"));
}
}
}
}
#endif

View File

@ -1,4 +1,6 @@
using System.Linq;
#if MA_VRCSDK3_AVATARS
using System.Linq;
using nadena.dev.ndmf;
using NUnit.Framework;
using UnityEditor.Animations;
@ -26,4 +28,6 @@ namespace modular_avatar_tests
Assert.AreEqual(desiredIndex, alc.layer);
}
}
}
}
#endif

View File

@ -1,9 +1,10 @@
using nadena.dev.modular_avatar.core.editor;
#if MA_VRCSDK3_AVATARS_3_5_2_OR_NEWER
using nadena.dev.modular_avatar.core.editor;
using NUnit.Framework;
using UnityEditor.Animations;
using VRC.SDK3.Avatars.Components;
#if MA_VRCSDK3_AVATARS_3_5_2_OR_NEWER
namespace modular_avatar_tests
{
public class PlayAudioRemapping : TestBase
@ -28,4 +29,5 @@ namespace modular_avatar_tests
}
}
}
#endif

View File

@ -4,7 +4,7 @@ using nadena.dev.modular_avatar.core.editor;
using NUnit.Framework;
using UnityEngine;
public class PreferFirstHipsMatch : TestBase
public class HipsMatchTest : TestBase
{
[Test]
public void SetupHeuristicPrefersFirstHipsMatch()
@ -21,7 +21,28 @@ public class PreferFirstHipsMatch : TestBase
var outfit = CreateChild(root, "Outfit");
var outfit_armature = CreateChild(outfit, "Armature");
var outfit_hips = CreateChild(outfit_armature, "Hips");
Assert.IsTrue(SetupOutfit.FindBones(outfit, out var det_av_root, out var det_av_hips, out var det_outfit_hips));
Assert.AreSame(root, det_av_root);
Assert.AreSame(root_hips, det_av_hips);
Assert.AreSame(outfit_hips, det_outfit_hips);
}
[Test]
public void TestOutfitDeepHipsMatch()
{
var root = CreateCommonPrefab("shapell.fbx");
#if MA_VRCSDK3_AVATARS
root.AddComponent<VRC.SDK3.Avatars.Components.VRCAvatarDescriptor>();
#endif
var root_hips = root.GetComponent<Animator>().GetBoneTransform(HumanBodyBones.Hips).gameObject;
root_hips.name = "hip";
var outfit = CreateChild(root, "Outfit");
var outfit_armature = CreateChild(outfit, "armature");
var outfit_armature2 = CreateChild(outfit_armature, "armature2");
var outfit_hips = CreateChild(outfit_armature2, "hips");
Assert.IsTrue(SetupOutfit.FindBones(outfit, out var det_av_root, out var det_av_hips, out var det_outfit_hips));
Assert.AreSame(root, det_av_root);
Assert.AreSame(root_hips, det_av_hips);

Some files were not shown because too many files have changed in this diff Show More