mirror of
https://github.com/bdunderscore/modular-avatar.git
synced 2024-12-29 18:55:06 +08:00
Merge branch 'main' into menu-richtext
This commit is contained in:
commit
6dcd63dde7
12
.github/ProjectRoot/vpm-manifest-2022.json
vendored
12
.github/ProjectRoot/vpm-manifest-2022.json
vendored
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
2
.github/workflows/build-release.yml
vendored
2
.github/workflows/build-release.yml
vendored
@ -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
|
||||||
|
2
.github/workflows/deploy-pages.yml
vendored
2
.github/workflows/deploy-pages.yml
vendored
@ -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 }}
|
||||||
|
1
.github/workflows/gameci.yml
vendored
1
.github/workflows/gameci.yml
vendored
@ -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"
|
||||||
|
@ -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)
|
||||||
{
|
{
|
||||||
|
@ -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)
|
||||||
|
@ -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 =
|
||||||
|
@ -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))
|
||||||
|
@ -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))
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fx.parameters = parameters;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ChildMotion GenerateDelayChild((EditorCurveBinding, string) binding)
|
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);
|
||||||
|
|
||||||
|
// Occasionally, we'll have a very small value pop up, probably due to FP errors.
|
||||||
|
// To correct for this, instead of directly using the property in the direct blend tree,
|
||||||
|
// we'll use a 1D blend tree to give ourselves a buffer.
|
||||||
|
|
||||||
|
var bufferBlendTree = new BlendTree();
|
||||||
|
bufferBlendTree.blendType = BlendTreeType.Simple1D;
|
||||||
|
bufferBlendTree.useAutomaticThresholds = false;
|
||||||
|
bufferBlendTree.blendParameter = prop;
|
||||||
|
bufferBlendTree.children = new[]
|
||||||
|
{
|
||||||
|
new ChildMotion
|
||||||
|
{
|
||||||
|
motion = nullMotion,
|
||||||
|
timeScale = 1,
|
||||||
|
threshold = 0
|
||||||
|
},
|
||||||
|
new ChildMotion
|
||||||
|
{
|
||||||
|
motion = nullMotion,
|
||||||
|
timeScale = 1,
|
||||||
|
threshold = 0.01f
|
||||||
|
},
|
||||||
|
new ChildMotion
|
||||||
|
{
|
||||||
|
motion = motion,
|
||||||
|
timeScale = 1,
|
||||||
|
threshold = 1
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return new ChildMotion
|
return new ChildMotion
|
||||||
{
|
{
|
||||||
motion = motion,
|
motion = bufferBlendTree,
|
||||||
directBlendParameter = prop,
|
directBlendParameter = MergeBlendTreePass.ALWAYS_ONE,
|
||||||
timeScale = 1
|
timeScale = 1
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
@ -368,21 +368,26 @@ namespace nadena.dev.modular_avatar.animation
|
|||||||
}
|
}
|
||||||
Profiler.EndSample();
|
Profiler.EndSample();
|
||||||
|
|
||||||
var layers = context.AvatarDescriptor.baseAnimationLayers
|
#if MA_VRCSDK3_AVATARS
|
||||||
.Concat(context.AvatarDescriptor.specialAnimationLayers);
|
if (context.AvatarDescriptor)
|
||||||
|
|
||||||
Profiler.BeginSample("ApplyMappingsToAvatarMasks");
|
|
||||||
foreach (var layer in layers)
|
|
||||||
{
|
{
|
||||||
ApplyMappingsToAvatarMask(layer.mask);
|
var layers = context.AvatarDescriptor.baseAnimationLayers
|
||||||
|
.Concat(context.AvatarDescriptor.specialAnimationLayers);
|
||||||
|
|
||||||
if (layer.animatorController is AnimatorController ac)
|
Profiler.BeginSample("ApplyMappingsToAvatarMasks");
|
||||||
// By this point, all AnimationOverrideControllers have been collapsed into an ephemeral
|
foreach (var layer in layers)
|
||||||
// AnimatorController so we can safely modify the controller in-place.
|
{
|
||||||
foreach (var acLayer in ac.layers)
|
ApplyMappingsToAvatarMask(layer.mask);
|
||||||
ApplyMappingsToAvatarMask(acLayer.avatarMask);
|
|
||||||
|
if (layer.animatorController is AnimatorController ac)
|
||||||
|
// By this point, all AnimationOverrideControllers have been collapsed into an ephemeral
|
||||||
|
// AnimatorController so we can safely modify the controller in-place.
|
||||||
|
foreach (var acLayer in ac.layers)
|
||||||
|
ApplyMappingsToAvatarMask(acLayer.avatarMask);
|
||||||
|
}
|
||||||
|
Profiler.EndSample();
|
||||||
}
|
}
|
||||||
Profiler.EndSample();
|
#endif
|
||||||
|
|
||||||
Profiler.EndSample();
|
Profiler.EndSample();
|
||||||
}
|
}
|
||||||
|
@ -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
|
@ -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)
|
||||||
|
@ -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;
|
||||||
|
@ -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); };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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,21 +368,65 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var otherName in bodyBones.SelectMany(bone => BoneToNameMap[bone]))
|
if (!isMapped)
|
||||||
{
|
{
|
||||||
if (lcNameToXform.TryGetValue(otherName, out var targetObject))
|
foreach (var bodyBone in bodyBones)
|
||||||
{
|
{
|
||||||
mappings[child] = targetObject;
|
if (avatarAnimator != null)
|
||||||
unassigned.Remove(targetObject);
|
{
|
||||||
lcNameToXform.Remove(otherName.ToLowerInvariant());
|
var avatarBone = avatarAnimator.GetBoneTransform(bodyBone);
|
||||||
break;
|
if (avatarBone != null && unassigned.Contains(avatarBone))
|
||||||
|
{
|
||||||
|
mappings[child] = avatarBone;
|
||||||
|
unassigned.Remove(avatarBone);
|
||||||
|
lcNameToXform.Remove(NormalizeName(avatarBone.gameObject.name));
|
||||||
|
isMapped = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isMapped)
|
||||||
|
{
|
||||||
|
foreach (var otherName in bodyBones.SelectMany(bone => BoneToNameMap[bone]))
|
||||||
|
{
|
||||||
|
if (lcNameToXform.TryGetValue(otherName, out var targetObject))
|
||||||
|
{
|
||||||
|
mappings[child] = targetObject;
|
||||||
|
unassigned.Remove(targetObject);
|
||||||
|
lcNameToXform.Remove(otherName.ToLowerInvariant());
|
||||||
|
isMapped = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -388,7 +445,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
return mappings;
|
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)
|
||||||
{
|
{
|
||||||
|
106
Editor/Inspector/DragAndDropManipulator.cs
Normal file
106
Editor/Inspector/DragAndDropManipulator.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
11
Editor/Inspector/DragAndDropManipulator.cs.meta
Normal file
11
Editor/Inspector/DragAndDropManipulator.cs.meta
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 528c660b56905844ea2f88bc73837e9f
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
@ -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
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
)
|
)
|
||||||
rootParameters[param.EffectiveName] = param;
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(param.EffectiveName))
|
||||||
|
{
|
||||||
|
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))
|
||||||
|
@ -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
|
@ -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();
|
||||||
|
@ -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;
|
||||||
|
@ -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,91 +50,25 @@ 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;
|
Undo.RecordObject(TargetComponent, "Add Toggled Objects");
|
||||||
_parentElem = target.parent;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void RegisterCallbacksOnTarget()
|
foreach (var reference in references)
|
||||||
{
|
|
||||||
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;
|
var toggledObject = new ToggledObject { Object = reference, Active = !reference.Get(TargetComponent).activeSelf };
|
||||||
|
TargetComponent.Objects.Add(toggledObject);
|
||||||
_parentElem.AddToClassList("drop-area--drag-active");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnDragUpdate(DragUpdatedEvent _)
|
|
||||||
{
|
|
||||||
if (_nowDragging.Length > 0) DragAndDrop.visualMode = DragAndDropVisualMode.Link;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnDragLeave(DragLeaveEvent evt)
|
|
||||||
{
|
|
||||||
_nowDragging = Array.Empty<GameObject>();
|
|
||||||
_parentElem.RemoveFromClassList("drop-area--drag-active");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnDragPerform(DragPerformEvent evt)
|
|
||||||
{
|
|
||||||
if (_nowDragging.Length > 0 && TargetComponent != null && _avatarRoot != null)
|
|
||||||
{
|
|
||||||
var knownObjs = TargetComponent.Objects.Select(o => o.Object.Get(TargetComponent)).ToHashSet();
|
|
||||||
|
|
||||||
Undo.RecordObject(TargetComponent, "Add Toggled Objects");
|
|
||||||
foreach (var obj in _nowDragging)
|
|
||||||
{
|
|
||||||
if (knownObjs.Contains(obj)) continue;
|
|
||||||
|
|
||||||
var aor = new AvatarObjectReference();
|
|
||||||
aor.Set(obj);
|
|
||||||
|
|
||||||
var toggledObject = new ToggledObject { Object = aor, Active = !obj.activeSelf };
|
|
||||||
TargetComponent.Objects.Add(toggledObject);
|
|
||||||
}
|
|
||||||
|
|
||||||
EditorUtility.SetDirty(TargetComponent);
|
|
||||||
PrefabUtility.RecordPrefabInstancePropertyModifications(TargetComponent);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_nowDragging = Array.Empty<GameObject>();
|
EditorUtility.SetDirty(TargetComponent);
|
||||||
_parentElem.RemoveFromClassList("drop-area--drag-active");
|
PrefabUtility.RecordPrefabInstancePropertyModifications(TargetComponent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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>();
|
||||||
|
@ -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))
|
||||||
{
|
{
|
||||||
_defaultValueField.value = 0;
|
if (!implicitUpdate)
|
||||||
|
{
|
||||||
|
_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));
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
#ListViewContainer {
|
#ListViewContainer {
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
|
max-height: 500px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.horizontal {
|
.horizontal {
|
||||||
|
@ -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>
|
39
Editor/Inspector/RemoveVertexColorEditor.cs
Normal file
39
Editor/Inspector/RemoveVertexColorEditor.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
3
Editor/Inspector/RemoveVertexColorEditor.cs.meta
Normal file
3
Editor/Inspector/RemoveVertexColorEditor.cs.meta
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: bfcaf601e9f94ba2900e66d66f469037
|
||||||
|
timeCreated: 1733085477
|
@ -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);
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
101
Editor/Inspector/SyncParameterSequenceEditor.cs
Normal file
101
Editor/Inspector/SyncParameterSequenceEditor.cs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
3
Editor/Inspector/SyncParameterSequenceEditor.cs.meta
Normal file
3
Editor/Inspector/SyncParameterSequenceEditor.cs.meta
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: bf6030b7fa704997885767897d1acba0
|
||||||
|
timeCreated: 1733090792
|
@ -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"
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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": "新しい共用パラメーターアセットを作成します"
|
||||||
}
|
}
|
||||||
|
@ -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": "清除所有覆寫",
|
||||||
|
@ -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 },
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
smr.rootBone = settings.RootBone;
|
|
||||||
smr.localBounds = settings.Bounds;
|
var settingsRootBone = settings.RootBone;
|
||||||
|
settingsRootBone = settingsRootBone == null ? smr.transform : settingsRootBone;
|
||||||
|
var smrRootBone = smr.rootBone;
|
||||||
|
smrRootBone = smrRootBone == null ? smr.transform : smrRootBone;
|
||||||
|
|
||||||
|
if (IsInverted(smrRootBone) != IsInverted(settingsRootBone))
|
||||||
|
{
|
||||||
|
smr.rootBone = GetInvertedRootBone(settingsRootBone);
|
||||||
|
|
||||||
|
var bounds = settings.Bounds;
|
||||||
|
var center = bounds.center;
|
||||||
|
center.x *= -1;
|
||||||
|
bounds.center = center;
|
||||||
|
smr.localBounds = bounds;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
smr.rootBone = settings.RootBone;
|
||||||
|
smr.localBounds = settings.Bounds;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool IsInverted(Transform bone)
|
||||||
|
{
|
||||||
|
var inverseCount = 0;
|
||||||
|
|
||||||
|
var scale = bone.lossyScale;
|
||||||
|
if (scale.x < 0) inverseCount += 1;
|
||||||
|
if (scale.y < 0) inverseCount += 1;
|
||||||
|
if (scale.z < 0) inverseCount += 1;
|
||||||
|
|
||||||
|
return (inverseCount % 2) != 0;
|
||||||
|
}
|
||||||
|
private Dictionary<Transform, Transform> invertedRootBoneCache = new();
|
||||||
|
private Transform GetInvertedRootBone(Transform rootBone)
|
||||||
|
{
|
||||||
|
if (invertedRootBoneCache.TryGetValue(rootBone, out var cache)) { return cache; }
|
||||||
|
|
||||||
|
var invertedRootBone = new GameObject($"{rootBone.gameObject.name}-InvertedRootBone");
|
||||||
|
EditorUtility.CopySerialized(rootBone, invertedRootBone.transform);
|
||||||
|
invertedRootBone.transform.parent = rootBone;
|
||||||
|
|
||||||
|
var transform = invertedRootBone.transform;
|
||||||
|
var scale = transform.localScale;
|
||||||
|
scale.x *= -1;
|
||||||
|
transform.localScale = scale;
|
||||||
|
|
||||||
|
invertedRootBoneCache[rootBone] = transform;
|
||||||
|
return transform;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
3
Editor/MiscPreview.meta
Normal file
3
Editor/MiscPreview.meta
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: ea61a438a5d54a289c6abbb1e05c56da
|
||||||
|
timeCreated: 1733085642
|
121
Editor/MiscPreview/RemoveVertexColorPreview.cs
Normal file
121
Editor/MiscPreview/RemoveVertexColorPreview.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
3
Editor/MiscPreview/RemoveVertexColorPreview.cs.meta
Normal file
3
Editor/MiscPreview/RemoveVertexColorPreview.cs.meta
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: b05d5c04f86b4924bf8acdd135448463
|
||||||
|
timeCreated: 1733085648
|
@ -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;
|
||||||
|
|
||||||
|
@ -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))
|
||||||
{
|
{
|
||||||
|
@ -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
|
||||||
};
|
};
|
||||||
|
@ -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>
|
||||||
{
|
{
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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,50 +197,80 @@ 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;
|
||||||
if (!shapeKeys.TryGetValue(key, out var info))
|
|
||||||
{
|
|
||||||
info = new AnimatedProperty(key, renderer.GetBlendShapeWeight(shapeId));
|
|
||||||
shapeKeys[key] = info;
|
|
||||||
|
|
||||||
// Add initial state
|
RegisterAction(key, currentValue, value, changer);
|
||||||
var agk = new ReactionRule(key, value);
|
|
||||||
agk.Value = renderer.GetBlendShapeWeight(shapeId);
|
if (_blendshapeSyncMappings.TryGetValue((renderer, shape.ShapeName), out var bindings))
|
||||||
info.actionGroups.Add(agk);
|
{
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var action = ObjectRule(key, changer, value);
|
key = new TargetProp
|
||||||
action.Inverted = _computeContext.Observe(changer, c => c.Inverted);
|
|
||||||
var isCurrentlyActive = changer.gameObject.activeInHierarchy;
|
|
||||||
|
|
||||||
if (shape.ChangeType == ShapeChangeType.Delete)
|
|
||||||
{
|
{
|
||||||
action.IsDelete = true;
|
TargetObject = renderer,
|
||||||
|
PropertyName = DeletedShapePrefix + shape.ShapeName
|
||||||
|
};
|
||||||
|
|
||||||
if (isCurrentlyActive) info.currentState = 100;
|
value = shape.ChangeType == ShapeChangeType.Delete ? 1 : 0;
|
||||||
|
RegisterAction(key, 0, value, changer);
|
||||||
info.actionGroups.Add(action); // Never merge
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (changer.gameObject.activeInHierarchy) info.currentState = action.Value;
|
|
||||||
|
|
||||||
if (info.actionGroups.Count == 0)
|
|
||||||
{
|
|
||||||
info.actionGroups.Add(action);
|
|
||||||
}
|
|
||||||
else if (!info.actionGroups[^1].TryMerge(action))
|
|
||||||
{
|
|
||||||
info.actionGroups.Add(action);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return shapeKeys;
|
return shapeKeys;
|
||||||
|
|
||||||
|
void RegisterAction(TargetProp key, float currentValue, float value, ModularAvatarShapeChanger changer)
|
||||||
|
{
|
||||||
|
if (!shapeKeys.TryGetValue(key, out var info))
|
||||||
|
{
|
||||||
|
info = new AnimatedProperty(key, currentValue);
|
||||||
|
shapeKeys[key] = info;
|
||||||
|
|
||||||
|
// Add initial state
|
||||||
|
var agk = new ReactionRule(key, value);
|
||||||
|
agk.Value = currentValue;
|
||||||
|
info.actionGroups.Add(agk);
|
||||||
|
}
|
||||||
|
|
||||||
|
var action = ObjectRule(key, changer, value);
|
||||||
|
action.Inverted = _computeContext.Observe(changer, c => c.Inverted);
|
||||||
|
|
||||||
|
if (changer.gameObject.activeInHierarchy) info.currentState = action.Value;
|
||||||
|
|
||||||
|
if (info.actionGroups.Count == 0)
|
||||||
|
{
|
||||||
|
info.actionGroups.Add(action);
|
||||||
|
}
|
||||||
|
else if (!info.actionGroups[^1].TryMerge(action))
|
||||||
|
{
|
||||||
|
info.actionGroups.Add(action);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void FindMaterialSetters(Dictionary<TargetProp, AnimatedProperty> objectGroups, GameObject root)
|
private void FindMaterialSetters(Dictionary<TargetProp, AnimatedProperty> objectGroups, GameObject root)
|
||||||
@ -245,3 +348,4 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
@ -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
|
@ -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
|
.Where(k => k >= 0)
|
||||||
.Select(i => mesh.GetBlendShapeIndex(i.PropertyName.Substring("blendShape.".Length)))
|
.ToList();
|
||||||
.Where(k => k >= 0)
|
|
||||||
.ToList()
|
renderer.sharedMesh = RemoveBlendShapeFromMesh.RemoveBlendshapes(mesh, shapesToDelete);
|
||||||
);
|
|
||||||
|
foreach (var name in shapeNamesToDelete)
|
||||||
|
{
|
||||||
|
// Don't need to animate this anymore...!
|
||||||
|
shapes.Remove(new TargetProp
|
||||||
|
{
|
||||||
|
TargetObject = renderer,
|
||||||
|
PropertyName = ReactiveObjectAnalyzer.BlendshapePrefix + name
|
||||||
|
});
|
||||||
|
|
||||||
|
shapes.Remove(new TargetProp
|
||||||
|
{
|
||||||
|
TargetObject = renderer,
|
||||||
|
PropertyName = ReactiveObjectAnalyzer.DeletedShapePrefix + name
|
||||||
|
});
|
||||||
|
|
||||||
|
initialStates.Remove(new TargetProp
|
||||||
|
{
|
||||||
|
TargetObject = renderer,
|
||||||
|
PropertyName = ReactiveObjectAnalyzer.BlendshapePrefix + name
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -257,10 +293,6 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
private void ProcessShapeKey(AnimatedProperty info)
|
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,30 +352,33 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
|
|
||||||
var conditions = GetTransitionConditions(asc, group);
|
var conditions = GetTransitionConditions(asc, group);
|
||||||
|
|
||||||
if (!group.Inverted)
|
foreach (var (st, transitions) in transitionBuffer)
|
||||||
{
|
{
|
||||||
var transition = new AnimatorStateTransition
|
if (!group.Inverted)
|
||||||
{
|
{
|
||||||
isExit = true,
|
var transition = new AnimatorStateTransition
|
||||||
hasExitTime = false,
|
|
||||||
duration = 0,
|
|
||||||
hasFixedDuration = true,
|
|
||||||
conditions = (AnimatorCondition[])conditions.Clone()
|
|
||||||
};
|
|
||||||
initialStateTransitionList.Add(transition);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
foreach (var cond in conditions)
|
|
||||||
{
|
|
||||||
initialStateTransitionList.Add(new AnimatorStateTransition
|
|
||||||
{
|
{
|
||||||
isExit = true,
|
isExit = true,
|
||||||
hasExitTime = false,
|
hasExitTime = false,
|
||||||
duration = 0,
|
duration = 0,
|
||||||
hasFixedDuration = true,
|
hasFixedDuration = true,
|
||||||
conditions = new[] { InvertCondition(cond) }
|
conditions = (AnimatorCondition[])conditions.Clone()
|
||||||
});
|
};
|
||||||
|
transitions.Add(transition);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach (var cond in conditions)
|
||||||
|
{
|
||||||
|
transitions.Add(new AnimatorStateTransition
|
||||||
|
{
|
||||||
|
isExit = true,
|
||||||
|
hasExitTime = false,
|
||||||
|
duration = 0,
|
||||||
|
hasFixedDuration = true,
|
||||||
|
conditions = new[] { InvertCondition(cond) }
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -582,3 +616,5 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endif
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
||||||
|
|
||||||
value = Math.Clamp(value, 0, 100);
|
if (isDelete)
|
||||||
|
{
|
||||||
|
if (value < 0.5f) continue;
|
||||||
|
value = -1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (states.ContainsKey(index))
|
||||||
|
{
|
||||||
|
// Delete takes precedence over set in preview
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (activeRule.IsDelete) value = -1;
|
value = Math.Clamp(value, 0, 100);
|
||||||
|
}
|
||||||
|
|
||||||
states = states.Add((index, value));
|
states = states.SetItem(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
|
@ -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
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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
|
93
Editor/RemoveVertexColorPass.cs
Normal file
93
Editor/RemoveVertexColorPass.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
3
Editor/RemoveVertexColorPass.cs.meta
Normal file
3
Editor/RemoveVertexColorPass.cs.meta
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: a227da6f9f1548c3867b1ed113f28e9d
|
||||||
|
timeCreated: 1733008734
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(merge.prefix) && string.IsNullOrEmpty(merge.suffix))
|
||||||
|
{
|
||||||
merge.InferPrefixSuffix();
|
merge.InferPrefixSuffix();
|
||||||
|
}
|
||||||
|
|
||||||
List<Transform> subRoots = new List<Transform>();
|
PrefabUtility.RecordPrefabInstancePropertyModifications(merge);
|
||||||
HeuristicBoneMapper.RenameBonesByHeuristic(merge, skipped: subRoots);
|
|
||||||
|
|
||||||
// If the outfit has an UpperChest bone but the avatar doesn't, add an additional MergeArmature to
|
var outfitAnimator = outfitRoot.GetComponent<Animator>();
|
||||||
// help with this
|
var outfitHumanoidBones = GetOutfitHumanoidBones(outfitRoot.transform, outfitAnimator);
|
||||||
foreach (var subRoot in subRoots)
|
var avatarAnimator = avatarRoot.GetComponent<Animator>();
|
||||||
|
List<Transform> subRoots = new List<Transform>();
|
||||||
|
HeuristicBoneMapper.RenameBonesByHeuristic(merge, skipped: subRoots, outfitHumanoidBones: outfitHumanoidBones, avatarAnimator: avatarAnimator);
|
||||||
|
|
||||||
|
// If the outfit has an UpperChest bone but the avatar doesn't, add an additional MergeArmature to
|
||||||
|
// help with this
|
||||||
|
foreach (var subRoot in subRoots)
|
||||||
|
{
|
||||||
|
var subConfig = subRoot.GetComponent<ModularAvatarMergeArmature>();
|
||||||
|
var subConfigMangleNames = false;
|
||||||
|
if (subConfig == null)
|
||||||
|
{
|
||||||
|
subConfig = Undo.AddComponent<ModularAvatarMergeArmature>(subRoot.gameObject);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Undo.RecordObject(subConfig, "");
|
||||||
|
subConfigMangleNames = subConfig.mangleNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subConfig.mergeTarget == null || subConfig.mergeTargetObject == null)
|
||||||
{
|
{
|
||||||
var subConfig = Undo.AddComponent<ModularAvatarMergeArmature>(subRoot.gameObject);
|
|
||||||
var parentTransform = subConfig.transform.parent;
|
var 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,34 +207,51 @@ 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);
|
||||||
if (merge.prefix == "" && merge.suffix == "" && avatarRootMatchingArmature != null)
|
if (merge.prefix == "" && merge.suffix == "" && avatarRootMatchingArmature != null)
|
||||||
{
|
{
|
||||||
// We have an armature whose names exactly match the root armature - this can cause some serious
|
// We have an armature whose names exactly match the root armature - this can cause some serious
|
||||||
// confusion in Unity's humanoid armature matching system. Fortunately, we can avoid this by
|
// confusion in Unity's humanoid armature matching system. Fortunately, we can avoid this by
|
||||||
// renaming a bone close to the root; this will ensure the number of matching bones is small, and
|
// renaming a bone close to the root; this will ensure the number of matching bones is small, and
|
||||||
// Unity's heuristics (apparently) will choose the base avatar's armature as the "true" armature.
|
// Unity's heuristics (apparently) will choose the base avatar's armature as the "true" armature.
|
||||||
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,22 +342,27 @@ namespace nadena.dev.modular_avatar.core.editor
|
|||||||
if (outfitArm == null) return;
|
if (outfitArm == null) return;
|
||||||
if (outfitLowerArm == null) return;
|
if (outfitLowerArm == null) return;
|
||||||
|
|
||||||
if ((avatarArm.position - outfitArm.position).magnitude > 0.001f) return;
|
if (strictMode)
|
||||||
|
{
|
||||||
|
if ((avatarArm.position - outfitArm.position).magnitude > 0.001f) return;
|
||||||
|
|
||||||
// check relative distance to lower arm as well
|
// check relative distance to lower arm as well
|
||||||
var avatarArmLength = (avatarLowerArm.position - avatarArm.position).magnitude;
|
var avatarArmLength = (avatarLowerArm.position - avatarArm.position).magnitude;
|
||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
118
Editor/SyncParameterSequencePass.cs
Normal file
118
Editor/SyncParameterSequencePass.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
3
Editor/SyncParameterSequencePass.cs.meta
Normal file
3
Editor/SyncParameterSequencePass.cs.meta
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 756425df8aeb4926afceda71bedffa40
|
||||||
|
timeCreated: 1733011801
|
@ -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
|
@ -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/).
|
||||||
|
@ -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
|
||||||
|
3
Runtime/ArmatureAwase/MoveIndep.meta
Normal file
3
Runtime/ArmatureAwase/MoveIndep.meta
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: abec4f397dc74b2f9bba6f71b5e702f3
|
||||||
|
timeCreated: 1732395066
|
647
Runtime/ArmatureAwase/MoveIndep/MAMoveIndependentlyManager.cs
Normal file
647
Runtime/ArmatureAwase/MoveIndep/MAMoveIndependentlyManager.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 154891b009044835b43580e745f50a9e
|
||||||
|
timeCreated: 1732394861
|
@ -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);
|
||||||
|
@ -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
|
||||||
|
@ -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
|
{
|
||||||
|
EditorApplication.delayCall += () =>
|
||||||
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,
|
if (this != null) MaMoveIndependentlyManager.Instance.Activate(this);
|
||||||
childLocalRot = localRotation,
|
|
||||||
childLocalScale = localScale,
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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()
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
|
|
||||||
var prefixLength = mergeName.IndexOf(baseName, StringComparison.InvariantCulture);
|
// Classic substring match
|
||||||
if (prefixLength < 0) return;
|
{
|
||||||
|
var prefixLength = mergeName.IndexOf(baseName, StringComparison.InvariantCulture);
|
||||||
|
if (prefixLength >= 0)
|
||||||
|
{
|
||||||
|
var suffixLength = mergeName.Length - prefixLength - baseName.Length;
|
||||||
|
|
||||||
var suffixLength = mergeName.Length - prefixLength - baseName.Length;
|
candidates.Add(new PSCandidate()
|
||||||
|
{
|
||||||
|
prefix = mergeName.Substring(0, prefixLength),
|
||||||
|
suffix = mergeName.Substring(mergeName.Length - suffixLength)
|
||||||
|
}.CountMatches(this));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
prefix = mergeName.Substring(0, prefixLength);
|
// Heuristic match - try to see if we get a better prefix/suffix pattern if we allow for fuzzy-matching of
|
||||||
suffix = mergeName.Substring(mergeName.Length - suffixLength);
|
// 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_")
|
||||||
{
|
{
|
||||||
|
31
Runtime/ModularAvatarSyncParameterSequence.cs
Normal file
31
Runtime/ModularAvatarSyncParameterSequence.cs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
11
Runtime/ModularAvatarSyncParameterSequence.cs.meta
Normal file
11
Runtime/ModularAvatarSyncParameterSequence.cs.meta
Normal 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:
|
@ -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();
|
||||||
|
@ -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();
|
||||||
|
@ -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)
|
||||||
|
23
Runtime/RemoveVertexColor.cs
Normal file
23
Runtime/RemoveVertexColor.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
11
Runtime/RemoveVertexColor.cs.meta
Normal file
11
Runtime/RemoveVertexColor.cs.meta
Normal 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:
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
Loading…
Reference in New Issue
Block a user