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": { "dependencies": {
"com.vrchat.avatars": { "com.vrchat.avatars": {
"version": "3.7.0" "version": "3.7.4"
}, },
"nadena.dev.ndmf": { "nadena.dev.ndmf": {
"version": "1.4.0" "version": "1.6.0"
} }
}, },
"locked": { "locked": {
"com.vrchat.avatars": { "com.vrchat.avatars": {
"version": "3.7.0", "version": "3.7.4",
"dependencies": { "dependencies": {
"com.vrchat.base": "3.7.0" "com.vrchat.base": "3.7.4"
} }
}, },
"com.vrchat.base": { "com.vrchat.base": {
"version": "3.7.0", "version": "3.7.4",
"dependencies": {} "dependencies": {}
}, },
"nadena.dev.ndmf": { "nadena.dev.ndmf": {
"version": "1.5.0" "version": "1.6.0"
} }
} }
} }

View File

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

View File

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

View File

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

View File

@ -53,7 +53,21 @@ namespace nadena.dev.modular_avatar.animation
set set
{ {
_originalClip = value; _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 #if MA_VRCSDK3_AVATARS
var avatarDescriptor = context.AvatarDescriptor; var avatarDescriptor = context.AvatarDescriptor;
if (!avatarDescriptor) return;
foreach (var layer in avatarDescriptor.baseAnimationLayers) foreach (var layer in avatarDescriptor.baseAnimationLayers)
{ {
@ -401,7 +416,7 @@ namespace nadena.dev.modular_avatar.animation
{ {
try try
{ {
AssetDatabase.AddObjectToAsset(curClip, _context.AssetContainer); _context.AssetSaver.SaveAsset(curClip);
} }
catch (Exception e) catch (Exception e)
{ {

View File

@ -7,7 +7,9 @@ using nadena.dev.ndmf;
using UnityEditor; using UnityEditor;
using UnityEditor.Animations; using UnityEditor.Animations;
using UnityEngine; using UnityEngine;
#if MA_VRCSDK3_AVATARS
using VRC.SDK3.Avatars.Components; using VRC.SDK3.Avatars.Components;
#endif
#endregion #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 // HACK: This is a temporary crutch until we rework the entire animator services system
public void AddPropertyDefinition(AnimatorControllerParameter paramDef) public void AddPropertyDefinition(AnimatorControllerParameter paramDef)
{ {
#if MA_VRCSDK3_AVATARS
if (!_context.AvatarDescriptor) return;
var fx = (AnimatorController) var fx = (AnimatorController)
_context.AvatarDescriptor.baseAnimationLayers _context.AvatarDescriptor.baseAnimationLayers
.First(l => l.type == VRCAvatarDescriptor.AnimLayerType.FX) .First(l => l.type == VRCAvatarDescriptor.AnimLayerType.FX)
.animatorController; .animatorController;
fx.parameters = fx.parameters.Concat(new[] { paramDef }).ToArray(); fx.parameters = fx.parameters.Concat(new[] { paramDef }).ToArray();
#endif
} }
public string GetActiveSelfProxy(GameObject obj) 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. // This helps reduce the risk that we'll accidentally modify the original assets.
#if MA_VRCSDK3_AVATARS #if MA_VRCSDK3_AVATARS
if (!context.AvatarDescriptor) return;
context.AvatarDescriptor.baseAnimationLayers = context.AvatarDescriptor.baseAnimationLayers =
CloneLayers(context, context.AvatarDescriptor.baseAnimationLayers); CloneLayers(context, context.AvatarDescriptor.baseAnimationLayers);
context.AvatarDescriptor.specialAnimationLayers = context.AvatarDescriptor.specialAnimationLayers =

View File

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

View File

@ -14,6 +14,7 @@ namespace nadena.dev.modular_avatar.animation
internal class DeepClone internal class DeepClone
{ {
private BuildContext _context;
private bool _isSaved; private bool _isSaved;
private UnityObject _combined; private UnityObject _combined;
@ -21,6 +22,7 @@ namespace nadena.dev.modular_avatar.animation
public DeepClone(BuildContext context) public DeepClone(BuildContext context)
{ {
_context = context;
_isSaved = context.AssetContainer != null && EditorUtility.IsPersistent(context.AssetContainer); _isSaved = context.AssetContainer != null && EditorUtility.IsPersistent(context.AssetContainer);
_combined = context.AssetContainer; _combined = context.AssetContainer;
} }
@ -33,6 +35,8 @@ namespace nadena.dev.modular_avatar.animation
if (original == null) return null; if (original == null) return null;
if (cloneMap == null) cloneMap = new Dictionary<UnityObject, UnityObject>(); if (cloneMap == null) cloneMap = new Dictionary<UnityObject, UnityObject>();
using var scope = _context.OpenSerializationScope();
Func<UnityObject, UnityObject> visitor = null; Func<UnityObject, UnityObject> visitor = null;
if (basePath != null) if (basePath != null)
{ {
@ -96,14 +100,12 @@ namespace nadena.dev.modular_avatar.animation
if (_isSaved && !EditorUtility.IsPersistent(obj)) if (_isSaved && !EditorUtility.IsPersistent(obj))
{ {
AssetDatabase.AddObjectToAsset(obj, _combined); scope.SaveAsset(obj);
} }
return (T)obj; return (T)obj;
} }
var ctor = original.GetType().GetConstructor(Type.EmptyTypes); var ctor = original.GetType().GetConstructor(Type.EmptyTypes);
if (ctor == null || original is ScriptableObject) if (ctor == null || original is ScriptableObject)
{ {
@ -120,7 +122,7 @@ namespace nadena.dev.modular_avatar.animation
if (_isSaved) if (_isSaved)
{ {
AssetDatabase.AddObjectToAsset(obj, _combined); scope.SaveAsset(obj);
} }
SerializedObject so = new SerializedObject(obj); SerializedObject so = new SerializedObject(obj);
@ -233,7 +235,7 @@ namespace nadena.dev.modular_avatar.animation
newClip.name = "rebased " + clip.name; newClip.name = "rebased " + clip.name;
if (_isSaved) if (_isSaved)
{ {
AssetDatabase.AddObjectToAsset(newClip, _combined); _context.AssetSaver.SaveAsset(newClip);
} }
foreach (var binding in AnimationUtility.GetCurveBindings(clip)) 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 nadena.dev.ndmf;
using UnityEditor; using UnityEditor;
using UnityEditor.Animations; using UnityEditor.Animations;
using UnityEngine; using UnityEngine;
using VRC.SDK3.Avatars.Components; using VRC.SDK3.Avatars.Components;
using BuildContext = nadena.dev.ndmf.BuildContext;
namespace nadena.dev.modular_avatar.animation namespace nadena.dev.modular_avatar.animation
{ {
@ -23,11 +26,16 @@ namespace nadena.dev.modular_avatar.animation
if (fx == null) return; if (fx == null) return;
var nullMotion = new AnimationClip();
nullMotion.name = "NullMotion";
var blendTree = new BlendTree(); var blendTree = new BlendTree();
blendTree.blendType = BlendTreeType.Direct; blendTree.blendType = BlendTreeType.Direct;
blendTree.useAutomaticThresholds = false; 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 asm = new AnimatorStateMachine();
var state = new AnimatorState(); var state = new AnimatorState();
@ -52,9 +60,24 @@ namespace nadena.dev.modular_avatar.animation
defaultWeight = 1, defaultWeight = 1,
blendingMode = AnimatorLayerBlendingMode.Override blendingMode = AnimatorLayerBlendingMode.Override
}).ToArray(); }).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;
}
} }
private ChildMotion GenerateDelayChild((EditorCurveBinding, string) binding) fx.parameters = parameters;
}
private ChildMotion GenerateDelayChild(Motion nullMotion, (EditorCurveBinding, string) binding)
{ {
var ecb = binding.Item1; var ecb = binding.Item1;
var prop = binding.Item2; var prop = binding.Item2;
@ -64,12 +87,43 @@ namespace nadena.dev.modular_avatar.animation
curve.AddKey(0, 1); curve.AddKey(0, 1);
AnimationUtility.SetEditorCurve(motion, ecb, curve); AnimationUtility.SetEditorCurve(motion, ecb, curve);
return new ChildMotion // 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, motion = motion,
directBlendParameter = prop, timeScale = 1,
threshold = 1
}
};
return new ChildMotion
{
motion = bufferBlendTree,
directBlendParameter = MergeBlendTreePass.ALWAYS_ONE,
timeScale = 1 timeScale = 1
}; };
} }
} }
} }
#endif

View File

@ -368,6 +368,9 @@ namespace nadena.dev.modular_avatar.animation
} }
Profiler.EndSample(); Profiler.EndSample();
#if MA_VRCSDK3_AVATARS
if (context.AvatarDescriptor)
{
var layers = context.AvatarDescriptor.baseAnimationLayers var layers = context.AvatarDescriptor.baseAnimationLayers
.Concat(context.AvatarDescriptor.specialAnimationLayers); .Concat(context.AvatarDescriptor.specialAnimationLayers);
@ -383,6 +386,8 @@ namespace nadena.dev.modular_avatar.animation
ApplyMappingsToAvatarMask(acLayer.avatarMask); ApplyMappingsToAvatarMask(acLayer.avatarMask);
} }
Profiler.EndSample(); Profiler.EndSample();
}
#endif
Profiler.EndSample(); Profiler.EndSample();
} }

View File

@ -1,4 +1,5 @@
#region #if MA_VRCSDK3_AVATARS
#region
using System; using System;
using System.Collections.Immutable; using System.Collections.Immutable;
@ -58,3 +59,4 @@ 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; if (!SaveImmediate || AssetDatabase.IsMainAsset(obj) || AssetDatabase.IsSubAsset(obj)) return;
AssetDatabase.AddObjectToAsset(obj, AssetContainer); PluginBuildContext.AssetSaver.SaveAsset(obj);
} }
public AnimatorController CreateAnimator(AnimatorController toClone = null) 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) internal static void FixupExpressionsMenu(BuildContext context)
{ {
if (!context.AvatarDescriptor) return;
context.AvatarDescriptor.customExpressions = true; context.AvatarDescriptor.customExpressions = true;
var expressionsMenu = context.AvatarDescriptor.expressionsMenu; var expressionsMenu = context.AvatarDescriptor.expressionsMenu;

View File

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

@ -232,6 +232,9 @@ namespace nadena.dev.modular_avatar.core.editor
internal static readonly Regex Regex_VRM_Bone = new Regex(@"^([LRC])_(.*)$"); 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) internal static string NormalizeName(string name)
{ {
name = name.ToLowerInvariant(); name = name.ToLowerInvariant();
@ -243,6 +246,14 @@ namespace nadena.dev.modular_avatar.core.editor
internal static readonly ImmutableDictionary<string, List<HumanBodyBones>> NameToBoneMap; internal static readonly ImmutableDictionary<string, List<HumanBodyBones>> NameToBoneMap;
internal static readonly ImmutableDictionary<HumanBodyBones, ImmutableList<string>> BoneToNameMap; internal static readonly ImmutableDictionary<HumanBodyBones, ImmutableList<string>> BoneToNameMap;
[InitializeOnLoadMethod]
private static void InsertboneNamePatternsToRuntime()
{
ModularAvatarMergeArmature.boneNamePatterns = boneNamePatterns;
ModularAvatarMergeArmature.AllBoneNames = AllBoneNames;
ModularAvatarMergeArmature.NormalizeBoneName = NormalizeName;
}
static HeuristicBoneMapper() static HeuristicBoneMapper()
{ {
var pat_end_side = new Regex(@"[_\.]([LR])$"); var pat_end_side = new Regex(@"[_\.]([LR])$");
@ -306,7 +317,9 @@ namespace nadena.dev.modular_avatar.core.editor
GameObject src, GameObject src,
GameObject newParent, GameObject newParent,
List<Transform> skipped = null, 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>(); Dictionary<Transform, Transform> mappings = new Dictionary<Transform, Transform>();
@ -355,13 +368,55 @@ namespace nadena.dev.modular_avatar.core.editor
var childName = child.gameObject.name; var childName = child.gameObject.name;
var targetObjectName = childName.Substring(config.prefix.Length, var targetObjectName = childName.Substring(config.prefix.Length,
childName.Length - config.prefix.Length - config.suffix.Length); childName.Length - config.prefix.Length - config.suffix.Length);
List<HumanBodyBones> bodyBones = null;
var isMapped = false;
if (!NameToBoneMap.TryGetValue( if (outfitHumanoidBones != null && outfitHumanoidBones.TryGetValue(child, out var outfitHumanoidBone))
NormalizeName(targetObjectName), out var bodyBones)) {
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; continue;
} }
if (!isMapped)
{
foreach (var bodyBone in bodyBones)
{
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])) foreach (var otherName in bodyBones.SelectMany(bone => BoneToNameMap[bone]))
{ {
if (lcNameToXform.TryGetValue(otherName, out var targetObject)) if (lcNameToXform.TryGetValue(otherName, out var targetObject))
@ -369,9 +424,11 @@ namespace nadena.dev.modular_avatar.core.editor
mappings[child] = targetObject; mappings[child] = targetObject;
unassigned.Remove(targetObject); unassigned.Remove(targetObject);
lcNameToXform.Remove(otherName.ToLowerInvariant()); lcNameToXform.Remove(otherName.ToLowerInvariant());
isMapped = true;
break; break;
} }
} }
}
if (!mappings.ContainsKey(child) && bodyBones.Contains(HumanBodyBones.UpperChest) && skipped != null) if (!mappings.ContainsKey(child) && bodyBones.Contains(HumanBodyBones.UpperChest) && skipped != null)
{ {
@ -388,7 +445,7 @@ namespace nadena.dev.modular_avatar.core.editor
return mappings; 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)); var target = config.mergeTarget.Get(RuntimeUtil.FindAvatarTransformInParents(config.transform));
if (target == null) return; if (target == null) return;
@ -399,7 +456,7 @@ namespace nadena.dev.modular_avatar.core.editor
void Traverse(Transform src, Transform dst) 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) 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 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 StyleSheet uss;
[SerializeField] private VisualTreeAsset uxml; [SerializeField] private VisualTreeAsset uxml;
private DragAndDropManipulator _dragAndDropManipulator;
protected override void OnInnerInspectorGUI() protected override void OnInnerInspectorGUI()
{ {
@ -37,7 +38,44 @@ namespace nadena.dev.modular_avatar.core.editor.ShapeChanger
listView.showBoundCollectionSize = false; listView.showBoundCollectionSize = false;
listView.virtualizationMethod = CollectionVirtualizationMethod.DynamicHeight; listView.virtualizationMethod = CollectionVirtualizationMethod.DynamicHeight;
_dragAndDropManipulator = new DragAndDropManipulator(root.Q("group-box"), target as ModularAvatarMaterialSetter);
return root; 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 { #f-material {
flex-grow: 1; 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 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() private void OnEnable()
{ {
_installer = (ModularAvatarMenuInstaller) target; _installer = (ModularAvatarMenuInstaller) target;
@ -215,74 +238,7 @@ namespace nadena.dev.modular_avatar.core.editor
var avatar = commonAvatar; var avatar = commonAvatar;
if (avatar != null && InstallTargets.Count == 1 && GUILayout.Button(G("menuinstall.selectmenu"))) if (avatar != null && InstallTargets.Count == 1 && GUILayout.Button(G("menuinstall.selectmenu")))
{ {
AvMenuTreeViewWindow.Show(avatar, _installer, menu => OpenSelectMenu(avatar, installTo);
{
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();
});
} }
} }
@ -368,7 +324,79 @@ namespace nadena.dev.modular_avatar.core.editor
serializedObject.ApplyModifiedProperties(); 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) private string ObjectHierarchyOrder(Component arg)
@ -415,6 +443,9 @@ namespace nadena.dev.modular_avatar.core.editor
var group = installer.gameObject.AddComponent<ModularAvatarMenuGroup>(); var group = installer.gameObject.AddComponent<ModularAvatarMenuGroup>();
var menuRoot = new GameObject(); var menuRoot = new GameObject();
menuRoot.name = "Menu"; menuRoot.name = "Menu";
group.targetObject = menuRoot;
Undo.RegisterCreatedObjectUndo(menuRoot, "Extract menu"); Undo.RegisterCreatedObjectUndo(menuRoot, "Extract menu");
menuRoot.transform.SetParent(group.transform, false); menuRoot.transform.SetParent(group.transform, false);
foreach (var control in menu.controls) foreach (var control in menu.controls)

View File

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

View File

@ -84,6 +84,7 @@ namespace nadena.dev.modular_avatar.core.editor
} }
private bool posResetOptionFoldout = false; private bool posResetOptionFoldout = false;
private bool posReset_convertATPose = true;
private bool posReset_adjustRotation = false; private bool posReset_adjustRotation = false;
private bool posReset_adjustScale = false; private bool posReset_adjustScale = false;
private bool posReset_heuristicRootScale = true; private bool posReset_heuristicRootScale = true;
@ -99,7 +100,7 @@ namespace nadena.dev.modular_avatar.core.editor
{ {
serializedObject.ApplyModifiedProperties(); serializedObject.ApplyModifiedProperties();
if (target.mergeTargetObject != null && priorMergeTarget == null if (target.mergeTargetObject != null && priorMergeTarget != target.mergeTargetObject
&& string.IsNullOrEmpty(target.prefix) && string.IsNullOrEmpty(target.prefix)
&& string.IsNullOrEmpty(target.suffix)) && string.IsNullOrEmpty(target.suffix))
{ {
@ -114,7 +115,27 @@ namespace nadena.dev.modular_avatar.core.editor
{ {
if (GUILayout.Button(G("merge_armature.adjust_names"))) 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 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( posReset_adjustRotation = EditorGUILayout.ToggleLeft(
G("merge_armature.reset_pos.adjust_rotation"), G("merge_armature.reset_pos.adjust_rotation"),
posReset_adjustRotation); posReset_adjustRotation);
posReset_adjustScale = EditorGUILayout.ToggleLeft(G("merge_armature.reset_pos.adjust_scale"), posReset_adjustScale = EditorGUILayout.ToggleLeft(G("merge_armature.reset_pos.adjust_scale"),
posReset_adjustScale); posReset_adjustScale);
posReset_heuristicRootScale = EditorGUILayout.ToggleLeft(
G("merge_armature.reset_pos.heuristic_scale"),
posReset_heuristicRootScale);
if (GUILayout.Button(G("merge_armature.reset_pos.execute"))) 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) if (posReset_heuristicRootScale && !suppressRootScale)
{ {
AdjustRootScale(); AdjustRootScale();

View File

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

View File

@ -35,14 +35,12 @@ namespace nadena.dev.modular_avatar.core.editor.ShapeChanger
ROSimulatorButton.BindRefObject(root, target); ROSimulatorButton.BindRefObject(root, target);
var listView = root.Q<ListView>("Shapes"); var listView = root.Q<ListView>("Shapes");
_dragAndDropManipulator = new DragAndDropManipulator(listView)
{
TargetComponent = target as ModularAvatarObjectToggle
};
listView.showBoundCollectionSize = false; listView.showBoundCollectionSize = false;
listView.virtualizationMethod = CollectionVirtualizationMethod.DynamicHeight; listView.virtualizationMethod = CollectionVirtualizationMethod.DynamicHeight;
_dragAndDropManipulator = new DragAndDropManipulator(root.Q("group-box"), target as ModularAvatarObjectToggle);
return root; return root;
} }
@ -52,92 +50,26 @@ namespace nadena.dev.modular_avatar.core.editor.ShapeChanger
_dragAndDropManipulator.TargetComponent = target as ModularAvatarObjectToggle; _dragAndDropManipulator.TargetComponent = target as ModularAvatarObjectToggle;
} }
private class DragAndDropManipulator : PointerManipulator private class DragAndDropManipulator : DragAndDropManipulator<ModularAvatarObjectToggle>
{ {
public ModularAvatarObjectToggle TargetComponent; public DragAndDropManipulator(VisualElement targetElement, ModularAvatarObjectToggle targetComponent)
private GameObject[] _nowDragging = Array.Empty<GameObject>(); : base(targetElement, targetComponent) { }
private Transform _avatarRoot;
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;
}
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)
{
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"); Undo.RecordObject(TargetComponent, "Add Toggled Objects");
foreach (var obj in _nowDragging)
foreach (var reference in references)
{ {
if (knownObjs.Contains(obj)) continue; var toggledObject = new ToggledObject { Object = reference, Active = !reference.Get(TargetComponent).activeSelf };
var aor = new AvatarObjectReference();
aor.Set(obj);
var toggledObject = new ToggledObject { Object = aor, Active = !obj.activeSelf };
TargetComponent.Objects.Add(toggledObject); TargetComponent.Objects.Add(toggledObject);
} }
EditorUtility.SetDirty(TargetComponent); EditorUtility.SetDirty(TargetComponent);
PrefabUtility.RecordPrefabInstancePropertyModifications(TargetComponent); PrefabUtility.RecordPrefabInstancePropertyModifications(TargetComponent);
} }
_nowDragging = Array.Empty<GameObject>();
_parentElem.RemoveFromClassList("drop-area--drag-active");
}
} }
} }
} }

View File

@ -51,6 +51,12 @@
width: 60px; width: 60px;
} }
.drop-area--drag-active > ListView ScrollView { .drop-area--drag-active {
background-color: rgba(0, 255, 255, 0.1); 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;
using UnityEditor.UIElements; using UnityEditor.UIElements;
using UnityEngine; using UnityEngine;
using UnityEngine.UI;
using UnityEngine.UIElements; using UnityEngine.UIElements;
using VRC.SDK3.Avatars.ScriptableObjects;
using static nadena.dev.modular_avatar.core.editor.Localization; 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 namespace nadena.dev.modular_avatar.core.editor
{ {
@ -35,6 +39,37 @@ namespace nadena.dev.modular_avatar.core.editor
listView.showBoundCollectionSize = false; listView.showBoundCollectionSize = false;
listView.virtualizationMethod = CollectionVirtualizationMethod.DynamicHeight; 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"); unregisteredListView = root.Q<ListView>("UnregisteredParameters");
@ -129,9 +164,72 @@ namespace nadena.dev.modular_avatar.core.editor
} }
}; };
var importProp = root.Q<ObjectField>("p_import");
importProp.RegisterValueChangedCallback(evt =>
{
ImportValues(importProp);
importProp.SetValueWithoutNotify(null);
});
importProp.objectType = typeof(VRCExpressionParameters);
importProp.allowSceneObjects = false;
return root; 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() private void DetectParameters()
{ {
var known = new HashSet<string>(); var known = new HashSet<string>();

View File

@ -20,6 +20,7 @@ namespace nadena.dev.modular_avatar.core.editor
private readonly DropdownField _boolField; private readonly DropdownField _boolField;
private ParameterSyncType _syncType; private ParameterSyncType _syncType;
private bool _hasInitialBinding;
public DefaultValueField() public DefaultValueField()
{ {
@ -57,28 +58,39 @@ namespace nadena.dev.modular_avatar.core.editor
{ {
_numberField.style.display = DisplayStyle.Flex; _numberField.style.display = DisplayStyle.Flex;
_boolField.style.display = DisplayStyle.None; _boolField.style.display = DisplayStyle.None;
OnUpdateNumberValue(_numberField.value); OnUpdateNumberValue(_numberField.value, true);
} }
else else
{ {
_numberField.style.display = DisplayStyle.None; _numberField.style.display = DisplayStyle.None;
_boolField.style.display = DisplayStyle.Flex; _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)) if (string.IsNullOrWhiteSpace(value))
{
if (!implicitUpdate)
{ {
_defaultValueField.value = 0; _defaultValueField.value = 0;
}
theValue = _defaultValueField.value;
_hasExplicitDefaultValueField.value = false; _hasExplicitDefaultValueField.value = false;
} }
else if (float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsed) else if (float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsed)
&& !float.IsNaN(parsed) && !float.IsNaN(parsed)
&& !float.IsInfinity(parsed)) && !float.IsInfinity(parsed))
{ {
_defaultValueField.value = _syncType switch theValue = _defaultValueField.value = _syncType switch
{ {
ParameterSyncType.Int => Mathf.FloorToInt(Mathf.Clamp(parsed, 0, 255)), ParameterSyncType.Int => Mathf.FloorToInt(Mathf.Clamp(parsed, 0, 255)),
ParameterSyncType.Float => Mathf.Clamp(parsed, -1, 1), ParameterSyncType.Float => Mathf.Clamp(parsed, -1, 1),
@ -88,11 +100,15 @@ namespace nadena.dev.modular_avatar.core.editor
_hasExplicitDefaultValueField.value = true; _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; _defaultValueField.value = value == V_True ? 1 : 0;
_hasExplicitDefaultValueField.value = value != V_None; _hasExplicitDefaultValueField.value = value != V_None;
@ -101,6 +117,8 @@ namespace nadena.dev.modular_avatar.core.editor
private void UpdateVisibleField(float value, bool hasExplicitValue) private void UpdateVisibleField(float value, bool hasExplicitValue)
{ {
_hasInitialBinding = true;
if (hasExplicitValue || Mathf.Abs(value) > 0.0000001) if (hasExplicitValue || Mathf.Abs(value) > 0.0000001)
{ {
_numberField.SetValueWithoutNotify(value.ToString(CultureInfo.InvariantCulture)); _numberField.SetValueWithoutNotify(value.ToString(CultureInfo.InvariantCulture));

View File

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

View File

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

View File

@ -12,7 +12,6 @@
show-border="true" show-border="true"
show-foldout-header="false" show-foldout-header="false"
name="Parameters" name="Parameters"
item-height="100"
binding-path="parameters" binding-path="parameters"
style="flex-grow: 1;" style="flex-grow: 1;"
/> />
@ -33,5 +32,7 @@
/> />
</ui:Foldout> </ui:Foldout>
<editor:ObjectField name="p_import" label="merge_parameter.ui.importFromAsset" class="ndmf-tr"/>
<ma:LanguageSwitcherElement/> <ma:LanguageSwitcherElement/>
</UXML> </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 StyleSheet uss;
[SerializeField] private VisualTreeAsset uxml; [SerializeField] private VisualTreeAsset uxml;
private DragAndDropManipulator _dragAndDropManipulator;
private BlendshapeSelectWindow _window; private BlendshapeSelectWindow _window;
protected override void OnInnerInspectorGUI() protected override void OnInnerInspectorGUI()
@ -41,6 +42,8 @@ namespace nadena.dev.modular_avatar.core.editor.ShapeChanger
listView.showBoundCollectionSize = false; listView.showBoundCollectionSize = false;
listView.virtualizationMethod = CollectionVirtualizationMethod.DynamicHeight; 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... // The Add button callback isn't exposed publicly for some reason...
var field_addButton = typeof(BaseListView).GetField("m_AddButton", NonPublic | Instance); var field_addButton = typeof(BaseListView).GetField("m_AddButton", NonPublic | Instance);
var addButton = (Button)field_addButton.GetValue(listView); var addButton = (Button)field_addButton.GetValue(listView);
@ -50,6 +53,41 @@ namespace nadena.dev.modular_avatar.core.editor.ShapeChanger
return root; 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() private void OnDisable()
{ {
if (_window != null) DestroyImmediate(_window); if (_window != null) DestroyImmediate(_window);

View File

@ -68,3 +68,13 @@
.change-type-delete #f-value-delete { .change-type-delete #f-value-delete {
display: flex; 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.add_button": "Add",
"merge_parameter.ui.details": "Parameter Configuration", "merge_parameter.ui.details": "Parameter Configuration",
"merge_parameter.ui.overrideAnimatorDefaults": "Override Animator Defaults", "merge_parameter.ui.overrideAnimatorDefaults": "Override Animator Defaults",
"merge_parameter.ui.importFromAsset": "Import from asset",
"merge_armature.merge_target": "Merge Target", "merge_armature.merge_target": "Merge Target",
"merge_armature.merge_target.tooltip": "The armature (or subtree) to merge this object into", "merge_armature.merge_target.tooltip": "The armature (or subtree) to merge this object into",
"merge_armature.prefix": "Prefix", "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.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": "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.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_rotation": "Also set rotation to base avatar",
"merge_armature.reset_pos.adjust_scale": "Also set local scale to base avatar", "merge_armature.reset_pos.adjust_scale": "Also set local scale to base avatar",
"merge_armature.reset_pos.execute": "Do it!", "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.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": "[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.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": "[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_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", "validation.blendshape_sync.no_local_mesh": "[MA-1001] No mesh found on the renderer on this object",
@ -283,5 +288,17 @@
"ro_sim.effect_group.conditions": "Conditions", "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.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.add_button": "追加",
"merge_parameter.ui.details": "パラメーターの詳細設定", "merge_parameter.ui.details": "パラメーターの詳細設定",
"merge_parameter.ui.overrideAnimatorDefaults": "アニメーターでの初期値を設定", "merge_parameter.ui.overrideAnimatorDefaults": "アニメーターでの初期値を設定",
"merge_parameter.ui.importFromAsset": "アセットからインポートする",
"merge_armature.merge_target": "統合先", "merge_armature.merge_target": "統合先",
"merge_armature.merge_target.tooltip": "このオブジェクトを統合先のアーマチュアに統合します", "merge_armature.merge_target.tooltip": "このオブジェクトを統合先のアーマチュアに統合します",
"merge_armature.prefix": "接頭辞", "merge_armature.prefix": "接頭辞",
@ -82,6 +83,7 @@
"merge_armature.lockmode.bidirectional.body": "アバターと統合されるアーマチュアは常に同じ位置になります。元のアバターを操作するアニメーションを作る時に便利かもしれません。有効にするためには、統合されるアーマチュアの位置を統合先と同じにしておく必要があります。", "merge_armature.lockmode.bidirectional.body": "アバターと統合されるアーマチュアは常に同じ位置になります。元のアバターを操作するアニメーションを作る時に便利かもしれません。有効にするためには、統合されるアーマチュアの位置を統合先と同じにしておく必要があります。",
"merge_armature.reset_pos": "位置を元アバターに合わせてリセット", "merge_armature.reset_pos": "位置を元アバターに合わせてリセット",
"merge_armature.reset_pos.info": "衣装のボーンの位置をアバターのボーンの位置に合わせます。非対応衣装を導入する際、アバウトに位置を合わせるのに便利です。", "merge_armature.reset_pos.info": "衣装のボーンの位置をアバターのボーンの位置に合わせます。非対応衣装を導入する際、アバウトに位置を合わせるのに便利です。",
"merge_armature.reset_pos.convert_atpose": "Aポーズ/Tポーズを合わせる",
"merge_armature.reset_pos.adjust_rotation": "回転も合わせる", "merge_armature.reset_pos.adjust_rotation": "回転も合わせる",
"merge_armature.reset_pos.adjust_scale": "スケールも合わせる", "merge_armature.reset_pos.adjust_scale": "スケールも合わせる",
"merge_armature.reset_pos.execute": "実行", "merge_armature.reset_pos.execute": "実行",
@ -145,6 +147,9 @@
"error.rename_params.default_value_conflict:hint": "予測不可能な動作を避けるため、MA Parametersコンポーネントの初期値フィールドはパラメーター名毎に1つだけしか指定しないようにし、他のコンポーネントでは空白のままにしてください。複数の値が存在する場合、Modular Avatarは階層順で最初に指定された初期値を採用します。", "error.rename_params.default_value_conflict:hint": "予測不可能な動作を避けるため、MA Parametersコンポーネントの初期値フィールドはパラメーター名毎に1つだけしか指定しないようにし、他のコンポーネントでは空白のままにしてください。複数の値が存在する場合、Modular Avatarは階層順で最初に指定された初期値を採用します。",
"error.replace_object.null_target": "[MA-0008] 置き換え先が指定されていません", "error.replace_object.null_target": "[MA-0008] 置き換え先が指定されていません",
"error.replace_object.null_target:hint": "Replace Objectは置き換え先のオブジェクトを指定する必要があります。", "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": "[MA-1000] このオブジェクトにはSkinned Mesh Rendererがありません。",
"validation.blendshape_sync.no_local_renderer:hint": "Blendshape Syncは同じGameObject上の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がありますが、メッシュがありません。", "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.material.tooltip": "上記の Reactive Component がアクティブな時に設定されるマテリアル",
"ro_sim.effect_group.rule_inverted": "このルールの条件は反転されています", "ro_sim.effect_group.rule_inverted": "このルールの条件は反転されています",
"ro_sim.effect_group.rule_inverted.tooltip": "このルールは、いずれかの条件が満たされていない場合に適用されます", "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.inverse": "反轉條件",
"reactive_object.material-setter.set-to": "將材質設定為:", "reactive_object.material-setter.set-to": "將材質設定為:",
"menuitem.misc.add_toggle": "新增開關", "menuitem.misc.add_toggle": "新增開關",
"ro_sim.open_debugger_button": "開啟響應除錯工具", "ro_sim.open_debugger_button": "開啟 Reaction 除錯工具",
"ro_sim.window.title": "MA 響應除錯工具", "ro_sim.window.title": "MA 響應除錯工具",
"ro_sim.header.inspecting": "檢視物件", "ro_sim.header.inspecting": "檢視物件",
"ro_sim.header.clear_overrides": "清除所有覆寫", "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) internal static VRCExpressionsMenu.Control CloneControl(VRCExpressionsMenu.Control c)
{ {
var type = c.type != 0 ? c.type : VRCExpressionsMenu.Control.ControlType.Button;
return new VRCExpressionsMenu.Control() return new VRCExpressionsMenu.Control()
{ {
type = c.type, type = type,
name = c.name, name = c.name,
icon = c.icon, icon = c.icon,
parameter = new VRCExpressionsMenu.Control.Parameter() { name = c.parameter?.name }, parameter = new VRCExpressionsMenu.Control.Parameter() { name = c.parameter?.name },

View File

@ -65,6 +65,7 @@ namespace nadena.dev.modular_avatar.core.editor
mergeSessions.Clear(); mergeSessions.Clear();
var descriptor = avatarGameObject.GetComponent<VRCAvatarDescriptor>(); var descriptor = avatarGameObject.GetComponent<VRCAvatarDescriptor>();
if (!descriptor) return;
if (descriptor.baseAnimationLayers != null) InitSessions(descriptor.baseAnimationLayers); if (descriptor.baseAnimationLayers != null) InitSessions(descriptor.baseAnimationLayers);
if (descriptor.specialAnimationLayers != null) InitSessions(descriptor.specialAnimationLayers); if (descriptor.specialAnimationLayers != null) InitSessions(descriptor.specialAnimationLayers);
@ -246,6 +247,18 @@ namespace nadena.dev.modular_avatar.core.editor
var stateMachineQueue = new Queue<AnimatorStateMachine>(); var stateMachineQueue = new Queue<AnimatorStateMachine>();
foreach (var layer in controller.layers) 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); stateMachineQueue.Enqueue(layer.stateMachine);
} }

View File

@ -1,4 +1,4 @@
/* /*
* MIT License * MIT License
* *
* Copyright (c) 2022 bd_ * Copyright (c) 2022 bd_
@ -117,6 +117,24 @@ namespace nadena.dev.modular_avatar.core.editor
RetainBoneReferences(c as Component); 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); 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 UnityEngine;
using Object = UnityEngine.Object;
namespace nadena.dev.modular_avatar.core.editor 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.Inherit
or ModularAvatarMeshSettings.InheritMode.DontSet or ModularAvatarMeshSettings.InheritMode.DontSet
or ModularAvatarMeshSettings.InheritMode.SetOrInherit), _): 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 case (_, not (ModularAvatarMeshSettings.InheritMode.Set
or ModularAvatarMeshSettings.InheritMode.Inherit or ModularAvatarMeshSettings.InheritMode.Inherit
or ModularAvatarMeshSettings.InheritMode.DontSet or ModularAvatarMeshSettings.InheritMode.DontSet
or ModularAvatarMeshSettings.InheritMode.SetOrInherit)): 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 // If current value is came from Set or DontSet, it should not be changed
case (ModularAvatarMeshSettings.InheritMode.Set, _): case (ModularAvatarMeshSettings.InheritMode.Set, _):
@ -144,9 +148,57 @@ namespace nadena.dev.modular_avatar.core.editor
if (newMesh) context.SaveAsset(newMesh); if (newMesh) context.SaveAsset(newMesh);
} }
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.rootBone = settings.RootBone;
smr.localBounds = settings.Bounds; 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,7 +11,19 @@ namespace nadena.dev.modular_avatar.core
{ {
private static ComputeContext _context; 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] [InitializeOnLoadMethod]
private static void Init() private static void Init()
@ -19,22 +31,42 @@ namespace nadena.dev.modular_avatar.core
EditorApplication.delayCall += ProcessObjectReferences; EditorApplication.delayCall += ProcessObjectReferences;
EditorApplication.update += () => 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() 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 = new ComputeContext("ObjectReferenceFixer");
_context.InvokeOnInvalidate<object>(typeof(ObjectReferenceFixer), _ => ProcessObjectReferences()); _context.InvokeOnInvalidate<object>(typeof(ObjectReferenceFixer), _ => ProcessObjectReferences());
IEnumerable<IHaveObjReferences> withReferences = _context.GetComponentsByType<IHaveObjReferences>(); IEnumerable<IHaveObjReferences> withReferences = _context.GetComponentsByType<IHaveObjReferences>();
if (_lastStage != null) if (contentsRoot != null)
withReferences = withReferences =
withReferences.Concat( withReferences.Concat(
_context.GetComponentsInChildren<IHaveObjReferences>(_lastStage.prefabContentsRoot, true) _context.GetComponentsInChildren<IHaveObjReferences>(contentsRoot, true)
); );
foreach (var obj in withReferences) foreach (var obj in withReferences)
@ -56,10 +88,26 @@ namespace nadena.dev.modular_avatar.core
foreach (var (targetObject, referencePath, objRef) in references) foreach (var (targetObject, referencePath, objRef) in references)
{ {
if (targetObject == null) continue; var resolvedTarget = objRef.Get(component);
_context.ObservePath(targetObject.transform); 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; if (objRef.IsConsistent(avatar)) continue;

View File

@ -9,6 +9,8 @@ namespace nadena.dev.modular_avatar.core.editor
{ {
protected override void Execute(ndmf.BuildContext context) protected override void Execute(ndmf.BuildContext context)
{ {
if (!context.AvatarDescriptor) return;
var expParams = context.AvatarDescriptor.expressionParameters; var expParams = context.AvatarDescriptor.expressionParameters;
if (expParams != null && context.IsTemporaryAsset(expParams)) 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) public IEnumerable<ProvidedParameter> GetSuppliedParameters(ndmf.BuildContext context = null)
{ {
return _component.parameters.Select(p => return _component.parameters
.Select(p =>
{ {
AnimatorControllerParameterType paramType; AnimatorControllerParameterType paramType;
bool animatorOnly = false; bool animatorOnly = false;
@ -87,7 +88,7 @@ namespace nadena.dev.modular_avatar.core.editor
_component, PluginDefinition.Instance, paramType) _component, PluginDefinition.Instance, paramType)
{ {
IsAnimatorOnly = animatorOnly, IsAnimatorOnly = animatorOnly,
WantSynced = !p.localOnly, WantSynced = !p.localOnly && !animatorOnly,
IsHidden = p.internalParameter, IsHidden = p.internalParameter,
DefaultValue = p.defaultValue DefaultValue = p.defaultValue
}; };

View File

@ -2,7 +2,6 @@
using System; using System;
using nadena.dev.modular_avatar.animation; 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.core.editor.plugin;
using nadena.dev.modular_avatar.editor.ErrorReporting; using nadena.dev.modular_avatar.editor.ErrorReporting;
using nadena.dev.ndmf; using nadena.dev.ndmf;
@ -57,30 +56,41 @@ namespace nadena.dev.modular_avatar.core.editor.plugin
#endif #endif
seq.WithRequiredExtension(typeof(AnimationServicesContext), _s2 => seq.WithRequiredExtension(typeof(AnimationServicesContext), _s2 =>
{ {
#if MA_VRCSDK3_AVATARS
seq.Run("Shape Changer", ctx => new ReactiveObjectPass(ctx).Execute()) seq.Run("Shape Changer", ctx => new ReactiveObjectPass(ctx).Execute())
.PreviewingWith(new ShapeChangerPreview(), new ObjectSwitcherPreview(), new MaterialSetterPreview()); .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(MergeArmaturePluginPass.Instance);
seq.Run(BoneProxyPluginPass.Instance); seq.Run(BoneProxyPluginPass.Instance);
#if MA_VRCSDK3_AVATARS
seq.Run(VisibleHeadAccessoryPluginPass.Instance); seq.Run(VisibleHeadAccessoryPluginPass.Instance);
#endif
seq.Run("World Fixed Object", seq.Run("World Fixed Object",
ctx => new WorldFixedObjectProcessor().Process(ctx) ctx => new WorldFixedObjectProcessor().Process(ctx)
); );
seq.Run(ReplaceObjectPluginPass.Instance); seq.Run(ReplaceObjectPluginPass.Instance);
#if MA_VRCSDK3_AVATARS #if MA_VRCSDK3_AVATARS
seq.Run(BlendshapeSyncAnimationPluginPass.Instance); seq.Run(BlendshapeSyncAnimationPluginPass.Instance);
#endif
seq.Run(GameObjectDelayDisablePass.Instance); seq.Run(GameObjectDelayDisablePass.Instance);
#endif
seq.Run(ConstraintConverterPass.Instance); seq.Run(ConstraintConverterPass.Instance);
}); });
#if MA_VRCSDK3_AVATARS #if MA_VRCSDK3_AVATARS
seq.Run(MenuInstallPluginPass.Instance);
seq.Run(PhysbonesBlockerPluginPass.Instance); seq.Run(PhysbonesBlockerPluginPass.Instance);
seq.Run("Fixup Expressions Menu", ctx => seq.Run("Fixup Expressions Menu", ctx =>
{ {
var maContext = ctx.Extension<ModularAvatarContext>().BuildContext; var maContext = ctx.Extension<ModularAvatarContext>().BuildContext;
FixupExpressionsMenuPass.FixupExpressionsMenu(maContext); FixupExpressionsMenuPass.FixupExpressionsMenu(maContext);
}); });
seq.Run(SyncParameterSequencePass.Instance);
#endif #endif
seq.Run(RemoveVertexColorPass.Instance).PreviewingWith(new RemoveVertexColorPreview());
seq.Run(RebindHumanoidAvatarPass.Instance); seq.Run(RebindHumanoidAvatarPass.Instance);
seq.Run("Purge ModularAvatar components", ctx => 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> class VisibleHeadAccessoryPluginPass : MAPass<VisibleHeadAccessoryPluginPass>
{ {
protected override void Execute(ndmf.BuildContext context) protected override void Execute(ndmf.BuildContext context)
@ -214,6 +225,7 @@ namespace nadena.dev.modular_avatar.core.editor.plugin
new VisibleHeadAccessoryProcessor(MAContext(context)).Process(); new VisibleHeadAccessoryProcessor(MAContext(context)).Process();
} }
} }
#endif
class ReplaceObjectPluginPass : MAPass<ReplaceObjectPluginPass> class ReplaceObjectPluginPass : MAPass<ReplaceObjectPluginPass>
{ {

View File

@ -1,14 +1,14 @@
using System.Collections.Generic; using System;
using UnityEngine; using System.Collections.Generic;
using System.Linq;
using Object = UnityEngine.Object;
namespace nadena.dev.modular_avatar.core.editor namespace nadena.dev.modular_avatar.core.editor
{ {
internal class AnimatedProperty internal class AnimatedProperty
{ {
public TargetProp TargetProp { get; } public TargetProp TargetProp { get; }
public string ControlParam { get; set; }
public bool alwaysDeleted;
public object currentState; public object currentState;
// Objects which trigger deletion of this shape key. // Objects which trigger deletion of this shape key.
@ -25,5 +25,30 @@ namespace nadena.dev.modular_avatar.core.editor
TargetProp = key; TargetProp = key;
this.currentState = currentState; 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 namespace nadena.dev.modular_avatar.core.editor
{ {
internal class ControlCondition internal class ControlCondition
{ {
public string Parameter; public string Parameter;
public UnityEngine.Object DebugReference; public Object DebugReference;
public string DebugName; public string DebugName;
public bool IsConstant; public bool IsConstant;
@ -14,5 +16,31 @@ namespace nadena.dev.modular_avatar.core.editor
public bool IsConstantActive => InitiallyActive && IsConstant; public bool IsConstantActive => InitiallyActive && IsConstant;
public GameObject ReferenceObject; 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 System.Linq;
using nadena.dev.modular_avatar.animation;
using UnityEngine; using UnityEngine;
using Object = UnityEngine.Object;
namespace nadena.dev.modular_avatar.core.editor namespace nadena.dev.modular_avatar.core.editor
{ {
@ -10,7 +11,7 @@ namespace nadena.dev.modular_avatar.core.editor
public ReactionRule(TargetProp key, float value) public ReactionRule(TargetProp key, float value)
: this(key, (object)value) { } : this(key, (object)value) { }
public ReactionRule(TargetProp key, UnityEngine.Object value) public ReactionRule(TargetProp key, Object value)
: this(key, (object)value) { } : this(key, (object)value) { }
private ReactionRule(TargetProp key, object value) private ReactionRule(TargetProp key, object value)
@ -31,12 +32,14 @@ namespace nadena.dev.modular_avatar.core.editor
public bool InitiallyActive => public bool InitiallyActive =>
((ControllingConditions.Count == 0) || ControllingConditions.All(c => c.InitiallyActive)) ^ Inverted; ((ControllingConditions.Count == 0) || ControllingConditions.All(c => c.InitiallyActive)) ^ Inverted;
public bool IsDelete;
public bool Inverted; public bool Inverted;
public bool IsConstant => ControllingConditions.Count == 0 || ControllingConditions.All(c => c.IsConstant); public bool IsConstant => ControllingConditions.Count == 0
public bool IsConstantOn => IsConstant && InitiallyActive; || ControllingConditions.All(c => c.IsConstant)
|| ControllingConditions.Any(c => c.IsConstant && !c.InitiallyActive);
public bool IsConstantActive => IsConstant && InitiallyActive ^ Inverted;
public override string ToString() public override string ToString()
{ {
@ -55,9 +58,36 @@ namespace nadena.dev.modular_avatar.core.editor
} }
else return false; else return false;
if (!ControllingConditions.SequenceEqual(other.ControllingConditions)) return false; if (!ControllingConditions.SequenceEqual(other.ControllingConditions)) return false;
if (IsDelete || other.IsDelete) return false;
return true; 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 System.Linq;
using nadena.dev.ndmf.preview; using nadena.dev.ndmf.preview;
using UnityEngine; using UnityEngine;
@ -39,6 +41,77 @@ namespace nadena.dev.modular_avatar.core.editor
} }
} }
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) private void BuildConditions(Component controllingComponent, ReactionRule rule)
{ {
rule.ControllingObject = controllingComponent; rule.ControllingObject = controllingComponent;
@ -124,35 +197,68 @@ namespace nadena.dev.modular_avatar.core.editor
var key = new TargetProp var key = new TargetProp
{ {
TargetObject = renderer, TargetObject = renderer,
PropertyName = "blendShape." + shape.ShapeName, PropertyName = BlendshapePrefix + shape.ShapeName
}; };
var currentValue = renderer.GetBlendShapeWeight(shapeId);
var value = shape.ChangeType == ShapeChangeType.Delete ? 100 : shape.Value; var value = shape.ChangeType == ShapeChangeType.Delete ? 100 : shape.Value;
RegisterAction(key, currentValue, value, changer);
if (_blendshapeSyncMappings.TryGetValue((renderer, shape.ShapeName), out var bindings))
{
// 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;
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);
}
}
key = new TargetProp
{
TargetObject = renderer,
PropertyName = DeletedShapePrefix + shape.ShapeName
};
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)) if (!shapeKeys.TryGetValue(key, out var info))
{ {
info = new AnimatedProperty(key, renderer.GetBlendShapeWeight(shapeId)); info = new AnimatedProperty(key, currentValue);
shapeKeys[key] = info; shapeKeys[key] = info;
// Add initial state // Add initial state
var agk = new ReactionRule(key, value); var agk = new ReactionRule(key, value);
agk.Value = renderer.GetBlendShapeWeight(shapeId); agk.Value = currentValue;
info.actionGroups.Add(agk); info.actionGroups.Add(agk);
} }
var action = ObjectRule(key, changer, value); var action = ObjectRule(key, changer, value);
action.Inverted = _computeContext.Observe(changer, c => c.Inverted); action.Inverted = _computeContext.Observe(changer, c => c.Inverted);
var isCurrentlyActive = changer.gameObject.activeInHierarchy;
if (shape.ChangeType == ShapeChangeType.Delete)
{
action.IsDelete = true;
if (isCurrentlyActive) info.currentState = 100;
info.actionGroups.Add(action); // Never merge
continue;
}
if (changer.gameObject.activeInHierarchy) info.currentState = action.Value; if (changer.gameObject.activeInHierarchy) info.currentState = action.Value;
@ -167,9 +273,6 @@ namespace nadena.dev.modular_avatar.core.editor
} }
} }
return shapeKeys;
}
private void FindMaterialSetters(Dictionary<TargetProp, AnimatedProperty> objectGroups, GameObject root) private void FindMaterialSetters(Dictionary<TargetProp, AnimatedProperty> objectGroups, GameObject root)
{ {
var materialSetters = _computeContext.GetComponentsInChildren<ModularAvatarMaterialSetter>(root, true); var materialSetters = _computeContext.GetComponentsInChildren<ModularAvatarMaterialSetter>(root, true);
@ -245,3 +348,4 @@ 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.Collections.Immutable;
using System.Linq; using System.Linq;
using nadena.dev.modular_avatar.animation; using nadena.dev.modular_avatar.animation;
@ -19,6 +20,11 @@ namespace nadena.dev.modular_avatar.core.editor
private readonly AnimationServicesContext _asc; private readonly AnimationServicesContext _asc;
private Dictionary<string, float> _simulationInitialStates; 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; public ImmutableDictionary<string, float> ForcePropertyOverrides { get; set; } = ImmutableDictionary<string, float>.Empty;
public ImmutableDictionary<string, ModularAvatarMenuItem> ForceMenuItems { get; set; } = public ImmutableDictionary<string, ModularAvatarMenuItem> ForceMenuItems { get; set; } =
@ -58,7 +64,6 @@ namespace nadena.dev.modular_avatar.core.editor
{ {
public Dictionary<TargetProp, AnimatedProperty> Shapes; public Dictionary<TargetProp, AnimatedProperty> Shapes;
public Dictionary<TargetProp, object> InitialStates; public Dictionary<TargetProp, object> InitialStates;
public HashSet<TargetProp> DeletedShapes;
} }
private static PropCache<GameObject, AnalysisResult> _analysisCache; private static PropCache<GameObject, AnalysisResult> _analysisCache;
@ -86,7 +91,6 @@ namespace nadena.dev.modular_avatar.core.editor
/// </summary> /// </summary>
/// <param name="root">The avatar root</param> /// <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="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> /// <returns></returns>
public AnalysisResult Analyze( public AnalysisResult Analyze(
GameObject root GameObject root
@ -98,10 +102,11 @@ namespace nadena.dev.modular_avatar.core.editor
{ {
result.Shapes = new(); result.Shapes = new();
result.InitialStates = new(); result.InitialStates = new();
result.DeletedShapes = new();
return result; return result;
} }
LocateBlendshapeSyncs(root);
Dictionary<TargetProp, AnimatedProperty> shapes = FindShapes(root); Dictionary<TargetProp, AnimatedProperty> shapes = FindShapes(root);
FindObjectToggles(shapes, root); FindObjectToggles(shapes, root);
FindMaterialSetters(shapes, root); FindMaterialSetters(shapes, root);
@ -109,7 +114,7 @@ namespace nadena.dev.modular_avatar.core.editor
ApplyInitialStateOverrides(shapes); ApplyInitialStateOverrides(shapes);
AnalyzeConstants(shapes); AnalyzeConstants(shapes);
ResolveToggleInitialStates(shapes); ResolveToggleInitialStates(shapes);
PreprocessShapes(shapes, out result.InitialStates, out result.DeletedShapes); PreprocessShapes(shapes, out result.InitialStates);
result.Shapes = shapes; result.Shapes = shapes;
return result; return result;
@ -124,7 +129,7 @@ namespace nadena.dev.modular_avatar.core.editor
foreach (var cond in rule.ControllingConditions) foreach (var cond in rule.ControllingConditions)
{ {
var paramName = cond.Parameter; var paramName = cond.Parameter;
if (ForcePropertyOverrides.TryGetValue(paramName, out var value)) if (ForcePropertyOverrides?.TryGetValue(paramName, out var value) == true)
{ {
cond.InitialValue = value; cond.InitialValue = value;
} }
@ -165,7 +170,7 @@ namespace nadena.dev.modular_avatar.core.editor
group.actionGroups.RemoveAll(agk => agk.IsConstant && !agk.InitiallyActive); group.actionGroups.RemoveAll(agk => agk.IsConstant && !agk.InitiallyActive);
// Remove all action groups up until the last one where we're always on // 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) if (lastAlwaysOnGroup > 0)
group.actionGroups.RemoveRange(0, lastAlwaysOnGroup - 1); group.actionGroups.RemoveRange(0, lastAlwaysOnGroup - 1);
} }
@ -264,40 +269,27 @@ namespace nadena.dev.modular_avatar.core.editor
} }
/// <summary> /// <summary>
/// Determine initial state and deleted shapes for all properties /// Determine initial state for all properties
/// </summary> /// </summary>
/// <param name="shapes"></param> /// <param name="shapes"></param>
/// <param name="initialStates"></param> /// <param name="initialStates"></param>
/// <param name="deletedShapes"></param> private void PreprocessShapes(Dictionary<TargetProp, AnimatedProperty> shapes,
private void PreprocessShapes(Dictionary<TargetProp, AnimatedProperty> shapes, out Dictionary<TargetProp, object> initialStates, out HashSet<TargetProp> deletedShapes) 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 // 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. // corresponding mesh. If we can't, delete ops are merged into the main list of operations.
initialStates = new Dictionary<TargetProp, object>(); initialStates = new Dictionary<TargetProp, object>();
deletedShapes = new HashSet<TargetProp>();
foreach (var (key, info) in shapes.ToList()) foreach (var (key, info) in shapes.ToList())
{ {
if (info.actionGroups.Count == 0) if (info.actionGroups.Count == 0)
{ {
// never active control; ignore it entirely // never active control; ignore it entirely
shapes.Remove(key); if (OptimizeShapes) shapes.Remove(key);
continue; 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) var initialState = info.actionGroups.Where(agk => agk.InitiallyActive)
.Select(agk => agk.Value) .Select(agk => agk.Value)
.Prepend(info.currentState) // use scene state if everything is disabled .Prepend(info.currentState) // use scene state if everything is disabled
@ -308,9 +300,10 @@ namespace nadena.dev.modular_avatar.core.editor
// If we're now constant-on, we can skip animation generation // If we're now constant-on, we can skip animation generation
if (info.actionGroups[^1].IsConstant) 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;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq; using System.Linq;
using nadena.dev.modular_avatar.animation; using nadena.dev.modular_avatar.animation;
using UnityEditor; using UnityEditor;
@ -34,6 +34,8 @@ namespace nadena.dev.modular_avatar.core.editor
internal void Execute() 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, // 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. // and if mixed, use WD ON to maximize compatibility.
_writeDefaults = MergeAnimatorProcessor.ProbeWriteDefaults(FindFxController().animatorController as AnimatorController) ?? true; _writeDefaults = MergeAnimatorProcessor.ProbeWriteDefaults(FindFxController().animatorController as AnimatorController) ?? true;
@ -42,10 +44,11 @@ namespace nadena.dev.modular_avatar.core.editor
var shapes = analysis.Shapes; var shapes = analysis.Shapes;
var initialStates = analysis.InitialStates; var initialStates = analysis.InitialStates;
var deletedShapes = analysis.DeletedShapes;
GenerateActiveSelfProxies(shapes); GenerateActiveSelfProxies(shapes);
ProcessMeshDeletion(initialStates, shapes);
ProcessInitialStates(initialStates, shapes); ProcessInitialStates(initialStates, shapes);
ProcessInitialAnimatorVariables(shapes); ProcessInitialAnimatorVariables(shapes);
@ -53,8 +56,6 @@ namespace nadena.dev.modular_avatar.core.editor
{ {
ProcessShapeKey(groups); ProcessShapeKey(groups);
} }
ProcessMeshDeletion(deletedShapes);
} }
private void GenerateActiveSelfProxies(Dictionary<TargetProp, AnimatedProperty> shapes) private void GenerateActiveSelfProxies(Dictionary<TargetProp, AnimatedProperty> shapes)
@ -225,30 +226,65 @@ namespace nadena.dev.modular_avatar.core.editor
#region Mesh processing #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 var renderers = initialStates
.GroupBy( .Where(kvp => kvp.Key.PropertyName.StartsWith(ReactiveObjectAnalyzer.DeletedShapePrefix))
v => (SkinnedMeshRenderer) v.TargetObject .Where(kvp => kvp.Key.TargetObject is SkinnedMeshRenderer)
).ToImmutableDictionary( .Where(kvp => kvp.Value is float f && f > 0.5f)
g => (SkinnedMeshRenderer) g.Key, // Filter any non-constant keys
g => g.ToList() .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; if (renderer == null) continue;
var mesh = renderer.sharedMesh; var mesh = renderer.sharedMesh;
if (mesh == null) continue; if (mesh == null) continue;
renderer.sharedMesh = RemoveBlendShapeFromMesh.RemoveBlendshapes( var shapesToDelete = shapeNamesToDelete
mesh, .Select(shape => mesh.GetBlendShapeIndex(shape))
infos
.Select(i => mesh.GetBlendShapeIndex(i.PropertyName.Substring("blendShape.".Length)))
.Where(k => k >= 0) .Where(k => k >= 0)
.ToList() .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) private void ProcessShapeKey(AnimatedProperty info)
{ {
// TODO: prune non-animated keys // 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); var asm = GenerateStateMachine(info);
ApplyController(asm, "MA Responsive: " + info.TargetProp.TargetObject.name); 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 transitionBuffer = new List<(AnimatorState, List<AnimatorStateTransition>)>();
var entryTransitions = new List<AnimatorTransition>(); var entryTransitions = new List<AnimatorTransition>();
var initialStateTransitionList = new List<AnimatorStateTransition>(); transitionBuffer.Add((initialState, new List<AnimatorStateTransition>()));
transitionBuffer.Add((initialState, initialStateTransitionList));
foreach (var group in info.actionGroups.Skip(lastConstant)) foreach (var group in info.actionGroups.Skip(lastConstant))
{ {
@ -321,6 +352,8 @@ namespace nadena.dev.modular_avatar.core.editor
var conditions = GetTransitionConditions(asc, group); var conditions = GetTransitionConditions(asc, group);
foreach (var (st, transitions) in transitionBuffer)
{
if (!group.Inverted) if (!group.Inverted)
{ {
var transition = new AnimatorStateTransition var transition = new AnimatorStateTransition
@ -331,13 +364,13 @@ namespace nadena.dev.modular_avatar.core.editor
hasFixedDuration = true, hasFixedDuration = true,
conditions = (AnimatorCondition[])conditions.Clone() conditions = (AnimatorCondition[])conditions.Clone()
}; };
initialStateTransitionList.Add(transition); transitions.Add(transition);
} }
else else
{ {
foreach (var cond in conditions) foreach (var cond in conditions)
{ {
initialStateTransitionList.Add(new AnimatorStateTransition transitions.Add(new AnimatorStateTransition
{ {
isExit = true, isExit = true,
hasExitTime = false, hasExitTime = false,
@ -347,6 +380,7 @@ namespace nadena.dev.modular_avatar.core.editor
}); });
} }
} }
}
var state = new AnimatorState(); var state = new AnimatorState();
@ -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 UnityEditor.Animations;
using UnityEngine; using UnityEngine;
@ -14,11 +15,11 @@ namespace nadena.dev.modular_avatar.core.editor
protected override void Execute(ndmf.BuildContext context) protected override void Execute(ndmf.BuildContext context)
{ {
var hasShapeChanger = context.AvatarRootObject.GetComponentInChildren<ModularAvatarShapeChanger>() != null; var hasShapeChanger = context.AvatarRootObject.GetComponentInChildren<ModularAvatarShapeChanger>(true) != null;
var hasObjectSwitcher = var hasObjectSwitcher =
context.AvatarRootObject.GetComponentInChildren<ModularAvatarObjectToggle>() != null; context.AvatarRootObject.GetComponentInChildren<ModularAvatarObjectToggle>(true) != null;
var hasMaterialSetter = var hasMaterialSetter =
context.AvatarRootObject.GetComponentInChildren<ModularAvatarMaterialSetter>() != null; context.AvatarRootObject.GetComponentInChildren<ModularAvatarMaterialSetter>(true) != null;
if (hasShapeChanger || hasObjectSwitcher || hasMaterialSetter) if (hasShapeChanger || hasObjectSwitcher || hasMaterialSetter)
{ {
var clip = new AnimationClip(); var clip = new AnimationClip();
@ -54,3 +55,4 @@ 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.Collections.Immutable;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -145,3 +146,4 @@ 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.Generic;
using nadena.dev.ndmf; using nadena.dev.ndmf;
using nadena.dev.ndmf.preview; using nadena.dev.ndmf.preview;
@ -71,3 +72,4 @@ 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.Collections.Immutable;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -105,3 +106,4 @@ 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 System.Linq;
using nadena.dev.ndmf; using nadena.dev.ndmf;
using UnityEditor.Animations;
using UnityEngine; using UnityEngine;
using VRC.SDK3.Avatars.Components;
using VRC.SDK3.Avatars.ScriptableObjects; using VRC.SDK3.Avatars.ScriptableObjects;
namespace nadena.dev.modular_avatar.core.editor namespace nadena.dev.modular_avatar.core.editor
@ -141,6 +144,12 @@ namespace nadena.dev.modular_avatar.core.editor
{ {
mami.Control.value = defaultValue.GetValueOrDefault(); 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 else
{ {
while (usedValues.Contains(nextValue)) nextValue++; while (usedValues.Contains(nextValue)) nextValue++;
@ -185,6 +194,26 @@ namespace nadena.dev.modular_avatar.core.editor
expParams.parameters = expParams.parameters.Concat(newParameters.Values).ToArray(); 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( internal static ControlCondition AssignMenuItemParameter(
@ -205,7 +234,8 @@ namespace nadena.dev.modular_avatar.core.editor
if (simulationInitialStates != null) if (simulationInitialStates != null)
{ {
var isDefault = mami.isDefault; 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); isDefault = ReferenceEquals(mami, target);
if (isDefault) if (isDefault)
@ -237,3 +267,4 @@ namespace nadena.dev.modular_avatar.core.editor
} }
} }
} }
#endif

View File

@ -1,4 +1,5 @@
#region #if MA_VRCSDK3_AVATARS
#region
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@ -72,8 +73,8 @@ namespace nadena.dev.modular_avatar.core.editor
var analysis = ReactiveObjectAnalyzer.CachedAnalyze(context, avatarRoot); var analysis = ReactiveObjectAnalyzer.CachedAnalyze(context, avatarRoot);
var shapes = analysis.Shapes; var shapes = analysis.Shapes;
ImmutableDictionary<SkinnedMeshRenderer, ImmutableList<(int, float)>>.Builder rendererStates = var rendererStates =
ImmutableDictionary.CreateBuilder<SkinnedMeshRenderer, ImmutableList<(int, float)>>( ImmutableDictionary.CreateBuilder<SkinnedMeshRenderer, ImmutableDictionary<int, float>>(
); );
var avatarRootTransform = avatarRoot.transform; var avatarRootTransform = avatarRoot.transform;
@ -83,16 +84,29 @@ namespace nadena.dev.modular_avatar.core.editor
var target = prop.TargetProp; var target = prop.TargetProp;
if (target.TargetObject == null || target.TargetObject is not SkinnedMeshRenderer r) continue; if (target.TargetObject == null || target.TargetObject is not SkinnedMeshRenderer r) continue;
if (!r.transform.IsChildOf(avatarRootTransform)) 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; var mesh = r.sharedMesh;
if (mesh == null) continue; if (mesh == null) continue;
var shapeName = target.PropertyName.Substring("blendShape.".Length);
if (!rendererStates.TryGetValue(r, out var states)) if (!rendererStates.TryGetValue(r, out var states))
{ {
states = ImmutableList<(int, float)>.Empty; states = ImmutableDictionary<int, float>.Empty;
rendererStates[r] = states; rendererStates[r] = states;
} }
@ -101,16 +115,32 @@ namespace nadena.dev.modular_avatar.core.editor
var activeRule = prop.actionGroups.LastOrDefault(rule => rule.InitiallyActive); var activeRule = prop.actionGroups.LastOrDefault(rule => rule.InitiallyActive);
if (activeRule == null || activeRule.Value is not float value) continue; if (activeRule == null || activeRule.Value is not float value) continue;
if (activeRule.ControllingObject == null) continue; // default value is being inherited
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); value = Math.Clamp(value, 0, 100);
}
if (activeRule.IsDelete) value = -1; states = states.SetItem(index, value);
states = states.Add((index, value));
rendererStates[r] = states; 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) private IEnumerable<RenderGroup> ShapesToGroups(GameObject avatarRoot, ImmutableDictionary<SkinnedMeshRenderer, ImmutableList<(int, float)>> shapes)
@ -266,3 +296,4 @@ 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.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Linq; using System.Linq;
@ -256,7 +257,7 @@ namespace nadena.dev.modular_avatar.core.editor.Simulator
return; 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; e_debugInfo.style.display = DisplayStyle.Flex;
@ -264,6 +265,7 @@ namespace nadena.dev.modular_avatar.core.editor.Simulator
_lastComputeContext.InvokeOnInvalidate(this, MaybeRefreshUI); _lastComputeContext.InvokeOnInvalidate(this, MaybeRefreshUI);
var analysis = new ReactiveObjectAnalyzer(_lastComputeContext); var analysis = new ReactiveObjectAnalyzer(_lastComputeContext);
analysis.OptimizeShapes = false;
analysis.ForcePropertyOverrides = PropertyOverrides.Value; analysis.ForcePropertyOverrides = PropertyOverrides.Value;
analysis.ForceMenuItems = MenuItemOverrides.Value; analysis.ForceMenuItems = MenuItemOverrides.Value;
var result = analysis.Analyze(avatar.gameObject); 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_set_inactive = effectGroup.Q<VisualElement>("effect__set-inactive");
var f_value = effectGroup.Q<FloatField>("effect__value"); var f_value = effectGroup.Q<FloatField>("effect__value");
var f_material = effectGroup.Q<ObjectField>("effect__material"); 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.style.display = DisplayStyle.None;
f_target_component.SetEnabled(false); f_target_component.SetEnabled(false);
@ -504,9 +506,10 @@ namespace nadena.dev.modular_avatar.core.editor.Simulator
f_property.value = targetProp.PropertyName; f_property.value = targetProp.PropertyName;
f_property.style.display = DisplayStyle.Flex; f_property.style.display = DisplayStyle.Flex;
if (reactionRule.IsDelete) if (reactionRule.TargetProp.PropertyName.StartsWith(ReactiveObjectAnalyzer.DeletedShapePrefix))
{ {
f_delete.style.display = DisplayStyle.Flex; f_delete.style.display = DisplayStyle.Flex;
f_delete.value = reactionRule.Value is > 0.5f ? "DELETE" : "RETAIN";
} else if (reactionRule.Value is float f) } else if (reactionRule.Value is float f)
{ {
f_value.SetValueWithoutNotify(f); f_value.SetValueWithoutNotify(f);
@ -636,3 +639,4 @@ namespace nadena.dev.modular_avatar.core.editor.Simulator
} }
} }
} }
#endif

View File

@ -1,5 +1,7 @@
using nadena.dev.modular_avatar.core.editor; using nadena.dev.modular_avatar.core.editor;
#if MA_VRCSDK3_AVATARS
using nadena.dev.modular_avatar.core.editor.Simulator; using nadena.dev.modular_avatar.core.editor.Simulator;
#endif
using UnityEditor; using UnityEditor;
using UnityEngine; using UnityEngine;
using UnityEngine.UIElements; using UnityEngine.UIElements;
@ -42,11 +44,13 @@ namespace nadena.dev.modular_avatar.core.editor
private void OpenDebugger() private void OpenDebugger()
{ {
#if MA_VRCSDK3_AVATARS
GameObject target = Selection.activeGameObject; GameObject target = Selection.activeGameObject;
if (ReferenceObject is Component c) target = c.gameObject; if (ReferenceObject is Component c) target = c.gameObject;
else if (ReferenceObject is GameObject go) target = go; else if (ReferenceObject is GameObject go) target = go;
ROSimulator.OpenDebugger(target); 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 UnityEditor;
using UnityEngine.UIElements; using UnityEngine.UIElements;
@ -75,3 +77,4 @@ 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) if (ResolvedParameter.syncType == ParameterSyncType.NotSynced)
{ {
ResolvedParameter.syncType = info.ResolvedParameter.syncType; ResolvedParameter.syncType = info.ResolvedParameter.syncType;
ResolvedParameter.localOnly = info.ResolvedParameter.localOnly;
} else if (ResolvedParameter.syncType != info.ResolvedParameter.syncType && info.ResolvedParameter.syncType != ParameterSyncType.NotSynced) } else if (ResolvedParameter.syncType != info.ResolvedParameter.syncType && info.ResolvedParameter.syncType != ParameterSyncType.NotSynced)
{ {
TypeConflict = true; TypeConflict = true;
@ -159,6 +160,8 @@ namespace nadena.dev.modular_avatar.core.editor
public void OnPreprocessAvatar(GameObject avatar, BuildContext context) public void OnPreprocessAvatar(GameObject avatar, BuildContext context)
{ {
if (!context.AvatarDescriptor) return;
_context = context; _context = context;
var syncParams = WalkTree(avatar); var syncParams = WalkTree(avatar);
@ -732,6 +735,7 @@ namespace nadena.dev.modular_avatar.core.editor
ParameterConfig parameterConfig = param; ParameterConfig parameterConfig = param;
parameterConfig.nameOrPrefix = remapTo; parameterConfig.nameOrPrefix = remapTo;
parameterConfig.remapTo = remapTo; parameterConfig.remapTo = remapTo;
parameterConfig.localOnly = parameterConfig.localOnly || param.syncType == ParameterSyncType.NotSynced;
var info = new ParameterInfo() var info = new ParameterInfo()
{ {
ResolvedParameter = parameterConfig, ResolvedParameter = parameterConfig,

View File

@ -344,7 +344,7 @@ namespace nadena.dev.modular_avatar.core.editor
if (proxy == null) return; if (proxy == null) return;
var curParent = proxy.transform.parent ?? original.transform.parent; 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 // We need to remember this proxy so we can avoid destroying it when we destroy VirtualAvatarRoot
// in Dispose // in Dispose

View File

@ -8,6 +8,7 @@ using UnityEditor;
using UnityEngine; using UnityEngine;
using Object = UnityEngine.Object; using Object = UnityEngine.Object;
using static nadena.dev.modular_avatar.core.editor.Localization; using static nadena.dev.modular_avatar.core.editor.Localization;
using System;
#endregion #endregion
@ -145,25 +146,57 @@ namespace nadena.dev.modular_avatar.core.editor
out var avatarRoot, out var avatarHips, out var outfitHips) out var avatarRoot, out var avatarHips, out var outfitHips)
) return; ) return;
Undo.SetCurrentGroupName("Setup Outfit");
var avatarArmature = avatarHips.transform.parent; var avatarArmature = avatarHips.transform.parent;
var outfitArmature = outfitHips.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 = new AvatarObjectReference();
merge.mergeTarget.referencePath = RuntimeUtil.RelativePath(avatarRoot, avatarArmature.gameObject); merge.mergeTarget.referencePath = RuntimeUtil.RelativePath(avatarRoot, avatarArmature.gameObject);
merge.LockMode = ArmatureLockMode.BaseToMerge; merge.LockMode = ArmatureLockMode.BaseToMerge;
merge.InferPrefixSuffix(); }
if (string.IsNullOrEmpty(merge.prefix) && string.IsNullOrEmpty(merge.suffix))
{
merge.InferPrefixSuffix();
}
PrefabUtility.RecordPrefabInstancePropertyModifications(merge);
var outfitAnimator = outfitRoot.GetComponent<Animator>();
var outfitHumanoidBones = GetOutfitHumanoidBones(outfitRoot.transform, outfitAnimator);
var avatarAnimator = avatarRoot.GetComponent<Animator>();
List<Transform> subRoots = new List<Transform>(); List<Transform> subRoots = new List<Transform>();
HeuristicBoneMapper.RenameBonesByHeuristic(merge, skipped: subRoots); 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 // If the outfit has an UpperChest bone but the avatar doesn't, add an additional MergeArmature to
// help with this // help with this
foreach (var subRoot in subRoots) foreach (var subRoot in subRoots)
{ {
var subConfig = Undo.AddComponent<ModularAvatarMergeArmature>(subRoot.gameObject); 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 parentTransform = subConfig.transform.parent; var parentTransform = subConfig.transform.parent;
var parentConfig = parentTransform.GetComponentInParent<ModularAvatarMergeArmature>(); var parentConfig = parentTransform.GetComponentInParent<ModularAvatarMergeArmature>();
var parentMapping = parentConfig.MapBone(parentTransform); var parentMapping = parentConfig.MapBone(parentTransform);
@ -174,7 +207,9 @@ namespace nadena.dev.modular_avatar.core.editor
subConfig.LockMode = ArmatureLockMode.BaseToMerge; subConfig.LockMode = ArmatureLockMode.BaseToMerge;
subConfig.prefix = merge.prefix; subConfig.prefix = merge.prefix;
subConfig.suffix = merge.suffix; subConfig.suffix = merge.suffix;
subConfig.mangleNames = false; subConfig.mangleNames = subConfigMangleNames;
PrefabUtility.RecordPrefabInstancePropertyModifications(subConfig);
}
} }
var avatarRootMatchingArmature = avatarRoot.transform.Find(outfitArmature.gameObject.name); var avatarRootMatchingArmature = avatarRoot.transform.Find(outfitArmature.gameObject.name);
@ -187,21 +222,36 @@ namespace nadena.dev.modular_avatar.core.editor
outfitArmature.name += ".1"; outfitArmature.name += ".1";
// Also make sure to refresh the avatar's animator humanoid bone cache. // Also make sure to refresh the avatar's animator humanoid bone cache.
var avatarAnimator = avatarRoot.GetComponent<Animator>();
var humanDescription = avatarAnimator.avatar; var humanDescription = avatarAnimator.avatar;
avatarAnimator.avatar = null; avatarAnimator.avatar = null;
// ReSharper disable once Unity.InefficientPropertyAccess // ReSharper disable once Unity.InefficientPropertyAccess
avatarAnimator.avatar = humanDescription; avatarAnimator.avatar = humanDescription;
} }
}
FixAPose(avatarRoot, outfitArmature); FixAPose(avatarRoot, outfitArmature);
if (outfitRoot != null var meshSettings = outfitRoot.GetComponent<ModularAvatarMeshSettings>();
&& outfitRoot.GetComponent<ModularAvatarMeshSettings>() == null var mSInheritProbeAnchor = ModularAvatarMeshSettings.InheritMode.SetOrInherit;
&& outfitRoot.GetComponentInParent<ModularAvatarMeshSettings>() == null) 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; Transform rootBone = null, probeAnchor = null;
Bounds bounds = ModularAvatarMeshSettings.DEFAULT_BOUNDS; Bounds bounds = ModularAvatarMeshSettings.DEFAULT_BOUNDS;
@ -217,8 +267,8 @@ namespace nadena.dev.modular_avatar.core.editor
rootBone = avatarRoot.transform; rootBone = avatarRoot.transform;
} }
meshSettings.InheritProbeAnchor = ModularAvatarMeshSettings.InheritMode.SetOrInherit; meshSettings.InheritProbeAnchor = mSInheritProbeAnchor;
meshSettings.InheritBounds = ModularAvatarMeshSettings.InheritMode.SetOrInherit; meshSettings.InheritBounds = mSInheritBounds;
meshSettings.ProbeAnchor = new AvatarObjectReference(); meshSettings.ProbeAnchor = new AvatarObjectReference();
meshSettings.ProbeAnchor.referencePath = RuntimeUtil.RelativePath(avatarRoot, probeAnchor.gameObject); 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 = new AvatarObjectReference();
meshSettings.RootBone.referencePath = RuntimeUtil.RelativePath(avatarRoot, rootBone.gameObject); meshSettings.RootBone.referencePath = RuntimeUtil.RelativePath(avatarRoot, rootBone.gameObject);
meshSettings.Bounds = bounds; 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>(); var mergeArmature = outfitArmature.GetComponent<ModularAvatarMergeArmature>();
if (mergeArmature == null) return; if (mergeArmature == null) return;
@ -249,7 +332,7 @@ namespace nadena.dev.modular_avatar.core.editor
{ {
var lowerArm = (HumanBodyBones)((int)arm + 2); 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 avatarArm = rootAnimator.GetBoneTransform(arm);
var outfitArm = avatarToOutfit(avatarArm); var outfitArm = avatarToOutfit(avatarArm);
@ -259,6 +342,8 @@ namespace nadena.dev.modular_avatar.core.editor
if (outfitArm == null) return; if (outfitArm == null) return;
if (outfitLowerArm == null) return; if (outfitLowerArm == null) return;
if (strictMode)
{
if ((avatarArm.position - outfitArm.position).magnitude > 0.001f) return; if ((avatarArm.position - outfitArm.position).magnitude > 0.001f) return;
// check relative distance to lower arm as well // check relative distance to lower arm as well
@ -266,15 +351,18 @@ namespace nadena.dev.modular_avatar.core.editor
var outfitArmLength = (outfitLowerArm.position - outfitArm.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( var relRot = Quaternion.FromToRotation(
outfitLowerArm.position - outfitArm.position, outfitLowerArm.position - outfitArm.position,
avatarLowerArm.position - avatarArm.position avatarLowerArm.position - avatarArm.position
); );
outfitArm.rotation = relRot * outfitArm.rotation; outfitArm.rotation = relRot * outfitArm.rotation;
PrefabUtility.RecordPrefabInstancePropertyModifications(outfitArm); PrefabUtility.RecordPrefabInstancePropertyModifications(outfitArm);
EditorUtility.SetDirty(outfitArm);
} }
Transform avatarToOutfit(Transform avBone) Transform avatarToOutfit(Transform avBone)
@ -490,6 +578,7 @@ namespace nadena.dev.modular_avatar.core.editor
} }
var hipsCandidates = new List<string>(); var hipsCandidates = new List<string>();
var hipsExtraCandidateRoots = new List<Transform>();
if (outfitHips == null) if (outfitHips == null)
{ {
@ -498,6 +587,23 @@ namespace nadena.dev.modular_avatar.core.editor
foreach (Transform child in outfitRoot.transform) foreach (Transform child in outfitRoot.transform)
{ {
foreach (Transform tempHip in child) 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)) if (tempHip.name.Contains(avatarHips.name))
{ {
@ -511,6 +617,7 @@ namespace nadena.dev.modular_avatar.core.editor
} }
hipsCandidates.Add(avatarHips.name); hipsCandidates.Add(avatarHips.name);
hipsExtraCandidateRoots = new List<Transform>();
// If that doesn't work out, we'll check for heuristic bone mapper mappings. // If that doesn't work out, we'll check for heuristic bone mapper mappings.
foreach (var hbm in HeuristicBoneMapper.BoneToNameMap[HumanBodyBones.Hips]) foreach (var hbm in HeuristicBoneMapper.BoneToNameMap[HumanBodyBones.Hips])
@ -531,6 +638,25 @@ namespace nadena.dev.modular_avatar.core.editor
{ {
outfitHips = tempHip.gameObject; 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;
using System.Collections.Generic; 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 /// initially inactive in the scene (which can have high overhead if the user has a lot of inactive avatars in the
/// scene). /// scene).
/// </summary> /// </summary>
[AddComponentMenu("")] [AddComponentMenu("/")]
[ExecuteInEditMode] [ExecuteInEditMode]
[DefaultExecutionOrder(-9998)] [DefaultExecutionOrder(-9998)]
public class Activator : MonoBehaviour, IEditorOnly public class Activator : MonoBehaviour, IEditorOnly
@ -30,7 +30,7 @@ namespace nadena.dev.modular_avatar.core
} }
} }
[AddComponentMenu("")] [AddComponentMenu("/")]
[ExecuteInEditMode] [ExecuteInEditMode]
[DefaultExecutionOrder(-9997)] [DefaultExecutionOrder(-9997)]
public class AvatarActivator : MonoBehaviour, IEditorOnly 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 System.Collections.Generic;
using Unity.Collections; using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe; using Unity.Collections.LowLevel.Unsafe;
using UnityEngine;
#endregion #endregion
@ -15,12 +14,19 @@ namespace nadena.dev.modular_avatar.core.armature_lock
internal NativeArray<T> Array; internal NativeArray<T> Array;
public static implicit operator NativeArray<T>(NativeArrayRef<T> arrayRef) => arrayRef.Array; public static implicit operator NativeArray<T>(NativeArrayRef<T> arrayRef) => arrayRef.Array;
public int Length => Array.Length;
public void Dispose() public void Dispose()
{ {
Array.Dispose(); Array.Dispose();
} }
public T this[int key]
{
get => Array[key];
set => Array[key] = value;
}
public void Resize(int n) public void Resize(int n)
{ {
if (Array.Length == n) return; if (Array.Length == n) return;
@ -144,6 +150,11 @@ namespace nadena.dev.modular_avatar.core.armature_lock
// array). As such, we clamp the length, rather than throwing an exception. // array). As such, we clamp the length, rather than throwing an exception.
length = Math.Min(length, InUseMask.Array.Length - offset); length = Math.Min(length, InUseMask.Array.Length - offset);
if (length < 0)
{
throw new ArgumentException("negative length");
}
unsafe unsafe
{ {
UnsafeUtility.MemSet((byte*)InUseMask.Array.GetUnsafePtr() + offset, value ? (byte)1 : (byte)0, length); UnsafeUtility.MemSet((byte*)InUseMask.Array.GetUnsafePtr() + offset, value ? (byte)1 : (byte)0, length);

View File

@ -30,6 +30,11 @@ namespace nadena.dev.modular_avatar.core
} }
#endif #endif
internal static void InvalidateAll()
{
HIERARCHY_CHANGED_SEQ++;
}
public AvatarObjectReference Clone() public AvatarObjectReference Clone()
{ {
return new AvatarObjectReference return new AvatarObjectReference

View File

@ -1,13 +1,14 @@
using System; using System;
using System.Collections.Generic;
using nadena.dev.modular_avatar.core.armature_lock; using nadena.dev.modular_avatar.core.armature_lock;
using UnityEditor;
using UnityEngine; using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
#if MA_VRCSDK3_AVATARS #if MA_VRCSDK3_AVATARS
using VRC.SDKBase; using VRC.SDKBase;
#endif #endif
namespace nadena.dev.modular_avatar.core.ArmatureAwase namespace nadena.dev.modular_avatar.core
{ {
[ExecuteInEditMode] [ExecuteInEditMode]
[AddComponentMenu("Modular Avatar/MA Move Independently")] [AddComponentMenu("Modular Avatar/MA Move Independently")]
@ -22,227 +23,35 @@ namespace nadena.dev.modular_avatar.core.ArmatureAwase
public GameObject[] GroupedBones public GameObject[] GroupedBones
{ {
get => m_groupedBones.Clone() as GameObject[]; get => m_groupedBones?.Clone() as GameObject[] ?? Array.Empty<GameObject>();
set set
{ {
m_groupedBones = value.Clone() as GameObject[]; 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() 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 #if UNITY_EDITOR
Undo.RecordObject(child, Undo.GetCurrentGroupName()); if (!PrefabUtility.IsPartOfPrefabAsset(this))
#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()
{ {
childLocalPos = localPosition, EditorApplication.delayCall += () =>
childLocalRot = localRotation, {
childLocalScale = localScale, if (this != null) MaMoveIndependentlyManager.Instance.Activate(this);
childToRoot = parentToRoot * childTRS,
}; };
_children[child] = state;
} }
#endif
} }
private void OnEnable() private void OnEnable()
{ {
UpdateLoopController.OnMoveIndependentlyUpdate += OnUpdate; MaMoveIndependentlyManager.Instance.Activate(this);
} }
private void OnDisable() private void OnDisable()
{ {
UpdateLoopController.OnMoveIndependentlyUpdate -= OnUpdate; MaMoveIndependentlyManager.Instance.Deactivate(this);
}
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;
}
} }
} }
} }

View File

@ -1,5 +1,7 @@
#if MA_VRCSDK3_AVATARS #if MA_VRCSDK3_AVATARS
using System;
using JetBrains.Annotations;
using UnityEngine; using UnityEngine;
using VRC.SDK3.Avatars.ScriptableObjects; using VRC.SDK3.Avatars.ScriptableObjects;
@ -12,6 +14,16 @@ namespace nadena.dev.modular_avatar.core
public VRCExpressionsMenu menuToAppend; public VRCExpressionsMenu menuToAppend;
public VRCExpressionsMenu installTargetMenu; 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 // ReSharper disable once Unity.RedundantEventFunction
void Start() void Start()

View File

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

View File

@ -26,6 +26,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using nadena.dev.modular_avatar.core.armature_lock; using nadena.dev.modular_avatar.core.armature_lock;
using UnityEngine; using UnityEngine;
using UnityEngine.Serialization; 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")] [HelpURL("https://modular-avatar.nadena.dev/docs/reference/merge-armature?lang=auto")]
public class ModularAvatarMergeArmature : AvatarTagComponent, IHaveObjReferences 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 AvatarObjectReference mergeTarget = new AvatarObjectReference();
public GameObject mergeTargetObject => mergeTarget.Get(this); public GameObject mergeTargetObject => mergeTarget.Get(this);
@ -61,6 +67,9 @@ namespace nadena.dev.modular_avatar.core
public bool mangleNames = true; 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; private ArmatureLockController _lockController;
internal Transform MapBone(Transform bone) 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() public void InferPrefixSuffix()
{ {
// We only infer if targeting the armature (below the Hips bone) // 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) // We also require that the attached object has exactly one child (presumably the hips)
if (transform.childCount != 1) return; 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 // Infer the prefix and suffix by comparing the names of the mergeTargetObject's hips with the child of the
// GameObject we're attached to. // GameObject we're attached to.
var baseName = hips.name; var baseName = hips.name;
var mergeName = transform.GetChild(0).name; var mergeHips = transform.GetChild(0);
var mergeName = mergeHips.name;
// Classic substring match
{
var prefixLength = mergeName.IndexOf(baseName, StringComparison.InvariantCulture); var prefixLength = mergeName.IndexOf(baseName, StringComparison.InvariantCulture);
if (prefixLength < 0) return; if (prefixLength >= 0)
{
var suffixLength = mergeName.Length - prefixLength - baseName.Length; var suffixLength = mergeName.Length - prefixLength - baseName.Length;
prefix = mergeName.Substring(0, prefixLength); candidates.Add(new PSCandidate()
suffix = mergeName.Substring(mergeName.Length - suffixLength); {
prefix = mergeName.Substring(0, prefixLength),
suffix = mergeName.Substring(mergeName.Length - suffixLength)
}.CountMatches(this));
}
}
// 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_") if (prefix == "J_Bip_C_")
{ {

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")] [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 public class ModularAvatarMaterialSetter : ReactiveComponent, IHaveObjReferences
{ {
[SerializeField] private List<MaterialSwitchObject> m_objects = new(); [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")] [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 public class ModularAvatarObjectToggle : ReactiveComponent, IHaveObjReferences
{ {
[SerializeField] private List<ToggledObject> m_objects = new(); [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")] [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 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")] [SerializeField] [FormerlySerializedAs("Shapes")]
private List<ChangedShape> m_shapes = new(); 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() public IEnumerable<AvatarObjectReference> GetObjectReferences()
{ {
foreach (var shape in m_shapes) 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 NUnit.Framework;
using UnityEditor; using UnityEditor;
using UnityEngine; using UnityEngine;
@ -37,3 +39,5 @@ namespace modular_avatar_tests
} }
} }
} }
#endif

View File

@ -1,4 +1,6 @@
using System; #if MA_VRCSDK3_AVATARS
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using nadena.dev.ndmf; using nadena.dev.ndmf;
@ -159,3 +161,5 @@ namespace modular_avatar_tests
} }
} }
} }
#endif

View File

@ -1,4 +1,6 @@
using System.Linq; #if MA_VRCSDK3_AVATARS
using System.Linq;
using nadena.dev.ndmf; using nadena.dev.ndmf;
using NUnit.Framework; using NUnit.Framework;
using UnityEditor.Animations; using UnityEditor.Animations;
@ -27,3 +29,5 @@ namespace modular_avatar_tests
} }
} }
} }
#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 NUnit.Framework;
using UnityEditor.Animations; using UnityEditor.Animations;
using VRC.SDK3.Avatars.Components; using VRC.SDK3.Avatars.Components;
#if MA_VRCSDK3_AVATARS_3_5_2_OR_NEWER
namespace modular_avatar_tests namespace modular_avatar_tests
{ {
public class PlayAudioRemapping : TestBase public class PlayAudioRemapping : TestBase
@ -28,4 +29,5 @@ namespace modular_avatar_tests
} }
} }
} }
#endif #endif

View File

@ -4,7 +4,7 @@ using nadena.dev.modular_avatar.core.editor;
using NUnit.Framework; using NUnit.Framework;
using UnityEngine; using UnityEngine;
public class PreferFirstHipsMatch : TestBase public class HipsMatchTest : TestBase
{ {
[Test] [Test]
public void SetupHeuristicPrefersFirstHipsMatch() public void SetupHeuristicPrefersFirstHipsMatch()
@ -27,4 +27,25 @@ public class PreferFirstHipsMatch : TestBase
Assert.AreSame(root_hips, det_av_hips); Assert.AreSame(root_hips, det_av_hips);
Assert.AreSame(outfit_hips, det_outfit_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);
Assert.AreSame(outfit_hips, det_outfit_hips);
}
} }

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