feat: Sync Parameter Sequence

This commit is contained in:
bd_ 2024-12-01 12:30:49 -08:00
parent 2c3e24333a
commit 76eca08c22
16 changed files with 614 additions and 3 deletions

View File

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

View File

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

View File

@ -153,6 +153,7 @@
"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.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.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",
@ -287,5 +288,12 @@
"ro_sim.effect_group.conditions": "Conditions", "ro_sim.effect_group.conditions": "Conditions",
"remove-vertex-color.mode": "Mode", "remove-vertex-color.mode": "Mode",
"remove-vertex-color.mode.Remove": "Remove Vertex Colors", "remove-vertex-color.mode.Remove": "Remove Vertex Colors",
"remove-vertex-color.mode.DontRemove": "Keep Vertex Colors" "remove-vertex-color.mode.DontRemove": "Keep Vertex Colors",
"general.vrcsdk-required": "This component requires the VRCSDK to function.",
"sync-param-sequence.platform": "Primary Platform",
"sync-param-sequence.platform.tooltip": "When building for this platform, Modular Avatar will record all expression parameters for use on other platform builds",
"sync-param-sequence.parameters": "Common parameters asset",
"sync-param-sequence.parameters.tooltip": "The asset to store common parameters in. Do not use the same Expression Parameters that you have set in your avatar descriptor.",
"sync-param-sequence.create-asset": "New",
"sync-param-sequence.create-asset.tooltip": "Creates a new expression parameters asset"
} }

View File

@ -149,6 +149,7 @@
"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.replacing_replacement": "[MA-0009] 複数のReplace Objectコンポーネントで、同じ置き換え先を指定できません",
"error.replace_object.parent_of_target": "[MA-0010] このオブジェクトの親を置き換え先に指定できません", "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がありますが、メッシュがありません。",
@ -279,5 +280,12 @@
"ro_sim.effect_group.conditions": "条件", "ro_sim.effect_group.conditions": "条件",
"remove-vertex-color.mode": "モード", "remove-vertex-color.mode": "モード",
"remove-vertex-color.mode.Remove": "頂点カラーを削除する", "remove-vertex-color.mode.Remove": "頂点カラーを削除する",
"remove-vertex-color.mode.DontRemove": "頂点カラーを削除しない" "remove-vertex-color.mode.DontRemove": "頂点カラーを削除しない",
"general.vrcsdk-required": "このコンポーネントにはVRCSDKが必要です。",
"sync-param-sequence.platform": "主要プラットホーム",
"sync-param-sequence.platform.tooltip": "このプラットホームでビルドすると、他のプラットホームを合わせるためにパラメーターを記録します。",
"sync-param-sequence.parameters": "共用パラメーターアセット",
"sync-param-sequence.parameters.tooltip": "共用パラメーターがこのアセットに保持されます。アバターデスクリプターに使われるアセットを流用しないでください。",
"sync-param-sequence.create-asset": "新規作成",
"sync-param-sequence.create-asset.tooltip": "新しい共用パラメーターアセットを作成します"
} }

View File

@ -88,8 +88,9 @@ namespace nadena.dev.modular_avatar.core.editor.plugin
var maContext = ctx.Extension<ModularAvatarContext>().BuildContext; var maContext = ctx.Extension<ModularAvatarContext>().BuildContext;
FixupExpressionsMenuPass.FixupExpressionsMenu(maContext); FixupExpressionsMenuPass.FixupExpressionsMenu(maContext);
}); });
seq.Run(RemoveVertexColorPass.Instance).PreviewingWith(new RemoveVertexColorPreview()); 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 =>
{ {

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 7b09a147690448ac94d495e90c761c0d
timeCreated: 1733093978

View File

@ -0,0 +1,251 @@
#if MA_VRCSDK3_AVATARS
using modular_avatar_tests;
using nadena.dev.modular_avatar.core;
using nadena.dev.modular_avatar.core.editor;
using NUnit.Framework;
using UnityEditor;
using UnityEngine;
using VRC.SDK3.Avatars.Components;
using VRC.SDK3.Avatars.ScriptableObjects;
namespace UnitTests.SyncParameterSequence
{
public class SyncParameterSequenceTest : TestBase
{
[Test]
public void NonPrimaryPlatform()
{
ModularAvatarSyncParameterSequence.Platform platform;
switch (EditorUserBuildSettings.activeBuildTarget)
{
case BuildTarget.Android:
platform = ModularAvatarSyncParameterSequence.Platform.PC;
break;
default:
platform = ModularAvatarSyncParameterSequence.Platform.Android;
break;
}
var root = CreateRoot("root");
var avdesc = root.GetComponent<VRCAvatarDescriptor>();
var expParams = ScriptableObject.CreateInstance<VRCExpressionParameters>();
expParams.parameters = new[]
{
new VRCExpressionParameters.Parameter()
{
name = "p1",
valueType = VRCExpressionParameters.ValueType.Bool,
networkSynced = true,
defaultValue = 0.5f,
},
new VRCExpressionParameters.Parameter()
{
name = "p2",
valueType = VRCExpressionParameters.ValueType.Int,
networkSynced = true,
defaultValue = 0.5f,
}
};
var refParams = ScriptableObject.CreateInstance<VRCExpressionParameters>();
refParams.parameters = new[]
{
new VRCExpressionParameters.Parameter()
{
name = "p0",
valueType = VRCExpressionParameters.ValueType.Bool,
networkSynced = true
},
new VRCExpressionParameters.Parameter()
{
name = "p2",
valueType = VRCExpressionParameters.ValueType.Int,
networkSynced = true
}
};
var c = avdesc.gameObject.AddComponent<ModularAvatarSyncParameterSequence>();
c.PrimaryPlatform = platform;
c.Parameters = refParams;
avdesc.expressionParameters = expParams;
var context = CreateContext(root);
SyncParameterSequencePass.ExecuteStatic(context);
expParams = avdesc.expressionParameters;
Assert.AreEqual("p0", expParams.parameters[0].name);
Assert.AreEqual("p2", expParams.parameters[1].name);
Assert.AreEqual("p1", expParams.parameters[2].name);
Assert.IsTrue(Mathf.Approximately(0f, expParams.parameters[0].defaultValue));
Assert.IsTrue(Mathf.Approximately(0.5f, expParams.parameters[1].defaultValue));
Assert.IsTrue(Mathf.Approximately(0.5f, expParams.parameters[2].defaultValue));
Assert.AreEqual(2, refParams.parameters.Length);
}
[Test]
public void PrimaryPlatform()
{
ModularAvatarSyncParameterSequence.Platform platform;
switch (EditorUserBuildSettings.activeBuildTarget)
{
case BuildTarget.Android:
platform = ModularAvatarSyncParameterSequence.Platform.Android;
break;
default:
platform = ModularAvatarSyncParameterSequence.Platform.PC;
break;
}
var root = CreateRoot("root");
var avdesc = root.GetComponent<VRCAvatarDescriptor>();
var expParams = ScriptableObject.CreateInstance<VRCExpressionParameters>();
expParams.parameters = new[]
{
new VRCExpressionParameters.Parameter()
{
name = "p1",
valueType = VRCExpressionParameters.ValueType.Bool,
networkSynced = true,
defaultValue = 0.5f,
},
new VRCExpressionParameters.Parameter()
{
name = "p2",
valueType = VRCExpressionParameters.ValueType.Int,
networkSynced = true,
defaultValue = 0.5f,
},
new VRCExpressionParameters.Parameter() {
name = "notsynced",
valueType = VRCExpressionParameters.ValueType.Int,
networkSynced = false,
}
};
var refParams = ScriptableObject.CreateInstance<VRCExpressionParameters>();
refParams.parameters = new[]
{
new VRCExpressionParameters.Parameter()
{
name = "p0",
valueType = VRCExpressionParameters.ValueType.Bool,
networkSynced = true
},
new VRCExpressionParameters.Parameter()
{
name = "p2",
valueType = VRCExpressionParameters.ValueType.Int,
networkSynced = true
}
};
var c = avdesc.gameObject.AddComponent<ModularAvatarSyncParameterSequence>();
c.PrimaryPlatform = platform;
c.Parameters = refParams;
avdesc.expressionParameters = expParams;
var context = CreateContext(root);
SyncParameterSequencePass.ExecuteStatic(context);
expParams = avdesc.expressionParameters;
Assert.AreEqual("p0", expParams.parameters[0].name);
Assert.AreEqual("p2", expParams.parameters[1].name);
Assert.AreEqual("p1", expParams.parameters[2].name);
Assert.AreEqual("notsynced", expParams.parameters[3].name);
Assert.IsTrue(Mathf.Approximately(0f, expParams.parameters[0].defaultValue));
Assert.IsTrue(Mathf.Approximately(0.5f, expParams.parameters[1].defaultValue));
Assert.IsTrue(Mathf.Approximately(0.5f, expParams.parameters[2].defaultValue));
Assert.AreEqual(3, refParams.parameters.Length);
Assert.AreEqual("p0", refParams.parameters[0].name);
Assert.AreEqual("p2", refParams.parameters[1].name);
Assert.AreEqual("p1", refParams.parameters[2].name);
}
[Test]
public void PrimaryPlatformOverflow()
{
ModularAvatarSyncParameterSequence.Platform platform;
switch (EditorUserBuildSettings.activeBuildTarget)
{
case BuildTarget.Android:
platform = ModularAvatarSyncParameterSequence.Platform.Android;
break;
default:
platform = ModularAvatarSyncParameterSequence.Platform.PC;
break;
}
var root = CreateRoot("root");
var avdesc = root.GetComponent<VRCAvatarDescriptor>();
var expParams = ScriptableObject.CreateInstance<VRCExpressionParameters>();
expParams.parameters = new[]
{
new VRCExpressionParameters.Parameter()
{
name = "p1",
valueType = VRCExpressionParameters.ValueType.Bool,
networkSynced = true,
defaultValue = 0.5f,
},
new VRCExpressionParameters.Parameter()
{
name = "p2",
valueType = VRCExpressionParameters.ValueType.Int,
networkSynced = true,
defaultValue = 0.5f,
}
};
var refParams = ScriptableObject.CreateInstance<VRCExpressionParameters>();
var paramList = new System.Collections.Generic.List<VRCExpressionParameters.Parameter>();
for (int i = 0; i < VRCExpressionParameters.MAX_PARAMETER_COST; i++)
{
paramList.Add(new()
{
name = "b" + i,
valueType = VRCExpressionParameters.ValueType.Bool,
networkSynced = true
});
}
refParams.parameters = paramList.ToArray();
var c = avdesc.gameObject.AddComponent<ModularAvatarSyncParameterSequence>();
c.PrimaryPlatform = platform;
c.Parameters = refParams;
avdesc.expressionParameters = expParams;
var context = CreateContext(root);
SyncParameterSequencePass.ExecuteStatic(context);
expParams = avdesc.expressionParameters;
Assert.AreEqual(2, expParams.parameters.Length);
Assert.AreEqual("p1", expParams.parameters[0].name);
Assert.AreEqual("p2", expParams.parameters[1].name);
Assert.AreEqual(2, refParams.parameters.Length);
Assert.AreEqual("p1", refParams.parameters[0].name);
Assert.AreEqual("p2", refParams.parameters[1].name);
}
}
}
#endif

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 37fcaa6b7094408eac945c2887a1101e
timeCreated: 1733093994

View File

@ -0,0 +1,38 @@
# Sync Parameter Sequence
![Sync Parameter Sequence](sync-parameter-sequence.png)
On VRChat, it's necessary for parameters that are shared between different-platforms of an avatar (e.g. PC and Android)
to appear at the start of the expressions parameters list, and in the same order. This component adjusts the order of
your expressions parameters, and adds additional parameters where necessary, to ensure that your avatar syncs properly
between PC and Android.
## When should I use it?
You should use this component if you are uploading different versions of the same avatar to PC and Android, and both
versions make use of synced expressions parameters.
## When shouldn't I use it?
This component may have compatibility issues with certain VRCFury components, such as Parameter Compressor.
## How should I use it?
First, attach the Sync Parameter Sequence component to any object on your avatar. Then, click the New button to create
an asset to save the parameter sequence. On other platform variants of your avatar, attach the component, and select the
asset you just created. Upload on Android (or whichever platform you want to be the primary platform), then upload for
other platforms as well.
Whenever you upload your avatar on the platform listed as "Primary Platform", Modular Avatar will record its expression
parameters in this asset. Then, later, when you upload on some other platform, Modular Avatar will adjust the order of
the parameters to match the primary platform.
## Parameter limits
The Sync Parameter Sequence component will add additional parameters to your avatar if necessary to ensure that the
order of parameters matches between platforms. This may cause your avatar to exceed the maximum number of parameters,
in which case the build will fail.
To address this, you can clear the contents of the parameters asset to clear out obsolete parameters; otherwise, make
sure you don't have a lot of both android-only and PC-only parameters, because you'll end up using the combination of
both.

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@ -0,0 +1,32 @@
# Sync Parameter Sequence
![Sync Parameter Sequence](sync-parameter-sequence.png)
VRChatでは、異なるプラットフォーム間で共有されるパラメータPCとAndroidが、パラメータリストの先頭に、同じ順序で登録してある必要が
あります。このコンポーネントは、アバターがPCとAndroid間で正しく同期するために、表情パラメータの順序を調整し、必要に応じてパラメータを追加します。
## いつ使うべきか?
同じアバターの異なるバージョンをPCとAndroidにアップロードし、両方のバージョンが同期パラメータを使用する場合、このコンポーネントを使用すると
便利です。
## 使わない方がいい場合
このコンポーネントは、Parameter Compressorなど一部のVRCFuryコンポーネントと互換性問題がある可能性があります。
## 使い方
まず、Sync Parameter Sequenceコンポーネントをアバターの任意のオブジェクトに追加します。次に、「新規作成」ボタンをクリックして、パラメータ
順序を保存するアセットを作成します。アバターの他のプラットフォーム用のバージョンに同じくコンポーネントを追加し、作成したアセットをセットします。
Androidあるいは主要プラットホームで選択したプラットフォームでアップロードし、そのあとに他のプラットフォームにもアップロードします。
「主要プラットフォーム」として設定されたプラットフォームでアバターをアップロードするたびに、Modular Avatarはパラメータリストをアセットに
記録します。その後、他のプラットフォームにアップロードする際に、Modular Avatarはパラメータの順序を主要プラットフォームに合わせて調整します。
## パラメータ制限について
Sync Parameter Sequenceコンポーネントは、パラメータの順序が一致するように必要に応じてアバターにパラメータを追加します。これにより、
アバターがパラメータの最大数を超える可能性があり、ビルドが失敗することがあります。
解決するために、パラメータアセットの内容をクリアして、不要なパラメータを削除するか、Android専用のパラメータとPC専用のパラメータの
両方を多く持っていないことを確認してください。そうしないと、両方の組み合わせを登録して限界を超えることになります。

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB