diff --git a/Editor/Inspector/RemoveVertexColorEditor.cs b/Editor/Inspector/RemoveVertexColorEditor.cs new file mode 100644 index 00000000..8e086567 --- /dev/null +++ b/Editor/Inspector/RemoveVertexColorEditor.cs @@ -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 + { + 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(); + } + } +} \ No newline at end of file diff --git a/Editor/Inspector/RemoveVertexColorEditor.cs.meta b/Editor/Inspector/RemoveVertexColorEditor.cs.meta new file mode 100644 index 00000000..a4ba2d29 --- /dev/null +++ b/Editor/Inspector/RemoveVertexColorEditor.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: bfcaf601e9f94ba2900e66d66f469037 +timeCreated: 1733085477 \ No newline at end of file diff --git a/Editor/Localization/en-US.json b/Editor/Localization/en-US.json index e43aa771..cc468249 100644 --- a/Editor/Localization/en-US.json +++ b/Editor/Localization/en-US.json @@ -284,5 +284,8 @@ "ro_sim.effect_group.rule_inverted": "This rule is inverted", "ro_sim.effect_group.rule_inverted.tooltip": "This rule will be applied when one of its conditions is NOT met", - "ro_sim.effect_group.conditions": "Conditions" + "ro_sim.effect_group.conditions": "Conditions", + "remove-vertex-color.mode": "Mode", + "remove-vertex-color.mode.Remove": "Remove Vertex Colors", + "remove-vertex-color.mode.DontRemove": "Keep Vertex Colors" } diff --git a/Editor/Localization/ja-JP.json b/Editor/Localization/ja-JP.json index 28d0b228..14640e70 100644 --- a/Editor/Localization/ja-JP.json +++ b/Editor/Localization/ja-JP.json @@ -276,5 +276,8 @@ "ro_sim.effect_group.material.tooltip": "上記の Reactive Component がアクティブな時に設定されるマテリアル", "ro_sim.effect_group.rule_inverted": "このルールの条件は反転されています", "ro_sim.effect_group.rule_inverted.tooltip": "このルールは、いずれかの条件が満たされていない場合に適用されます", - "ro_sim.effect_group.conditions": "条件" + "ro_sim.effect_group.conditions": "条件", + "remove-vertex-color.mode": "モード", + "remove-vertex-color.mode.Remove": "頂点カラーを削除する", + "remove-vertex-color.mode.DontRemove": "頂点カラーを削除しない" } diff --git a/Editor/MiscPreview.meta b/Editor/MiscPreview.meta new file mode 100644 index 00000000..3e9c5ff4 --- /dev/null +++ b/Editor/MiscPreview.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: ea61a438a5d54a289c6abbb1e05c56da +timeCreated: 1733085642 \ No newline at end of file diff --git a/Editor/MiscPreview/RemoveVertexColorPreview.cs b/Editor/MiscPreview/RemoveVertexColorPreview.cs new file mode 100644 index 00000000..e1e8a4e0 --- /dev/null +++ b/Editor/MiscPreview/RemoveVertexColorPreview.cs @@ -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 GetTargetGroups(ComputeContext context) + { + var roots = context.GetAvatarRoots(); + var removers = roots + .SelectMany(r => context.GetComponentsInChildren(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(r, true) + .Concat( + context.GetComponentsInChildren(r, true) + .SelectMany(mf => context.GetComponents(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 Instantiate(RenderGroup group, IEnumerable<(Renderer, Renderer)> proxyPairs, + ComputeContext context) + { + Dictionary conversionMap = new(); + + foreach (var (_, proxy) in proxyPairs) + { + Component c = proxy; + if (!(c is SkinnedMeshRenderer)) + { + c = context.GetComponent(proxy.gameObject); + } + + if (c == null) continue; + + RemoveVertexColorPass.ForceRemove(_ => false, c, conversionMap); + } + + return Task.FromResult(new Node(conversionMap.Values.FirstOrDefault())); + } + + private class Node : IRenderFilterNode + { + private readonly Mesh? _theMesh; + + public Node(Mesh? theMesh) + { + _theMesh = theMesh; + } + + public Task Refresh(IEnumerable<(Renderer, Renderer)> proxyPairs, ComputeContext context, + RenderAspects updatedAspects) + { + if (updatedAspects.HasFlag(RenderAspects.Mesh)) return Task.FromResult(null); + if (_theMesh == null) return Task.FromResult(null); + + return Task.FromResult(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(); + if (mf != null) mf.sharedMesh = _theMesh; + break; + } + } + } + } + } +} \ No newline at end of file diff --git a/Editor/MiscPreview/RemoveVertexColorPreview.cs.meta b/Editor/MiscPreview/RemoveVertexColorPreview.cs.meta new file mode 100644 index 00000000..28b9cb3d --- /dev/null +++ b/Editor/MiscPreview/RemoveVertexColorPreview.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: b05d5c04f86b4924bf8acdd135448463 +timeCreated: 1733085648 \ No newline at end of file diff --git a/Editor/PluginDefinition/PluginDefinition.cs b/Editor/PluginDefinition/PluginDefinition.cs index 3bc7cf1b..9be466c9 100644 --- a/Editor/PluginDefinition/PluginDefinition.cs +++ b/Editor/PluginDefinition/PluginDefinition.cs @@ -88,6 +88,7 @@ namespace nadena.dev.modular_avatar.core.editor.plugin var maContext = ctx.Extension().BuildContext; FixupExpressionsMenuPass.FixupExpressionsMenu(maContext); }); + seq.Run(RemoveVertexColorPass.Instance).PreviewingWith(new RemoveVertexColorPreview()); #endif seq.Run(RebindHumanoidAvatarPass.Instance); seq.Run("Purge ModularAvatar components", ctx => diff --git a/Editor/RemoveVertexColorPass.cs b/Editor/RemoveVertexColorPass.cs new file mode 100644 index 00000000..3886b32c --- /dev/null +++ b/Editor/RemoveVertexColorPass.cs @@ -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 + { + protected override void Execute(ndmf.BuildContext context) + { + var removers = context.AvatarRootTransform.GetComponentsInChildren(true)!; + + Dictionary conversionMap = new(); + + foreach (var remover in removers) + { + foreach (var smr in remover!.GetComponentsInChildren(true)) + { + TryRemove(context.IsTemporaryAsset, smr, conversionMap); + } + + foreach (var mf in remover.GetComponentsInChildren(true)) + { + TryRemove(context.IsTemporaryAsset, mf, conversionMap); + } + } + } + + private const string PropPath = "m_Mesh"; + + private static void TryRemove( + Func isTempAsset, + Component c, + Dictionary conversionMap + ) + { + var nearestRemover = c.GetComponentInParent()!; + if (nearestRemover.Mode != ModularAvatarRemoveVertexColor.RemoveMode.Remove) return; + + ForceRemove(isTempAsset, c, conversionMap); + } + + internal static void ForceRemove(Func isTempAsset, Component c, + Dictionary 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; + } + } +} \ No newline at end of file diff --git a/Editor/RemoveVertexColorPass.cs.meta b/Editor/RemoveVertexColorPass.cs.meta new file mode 100644 index 00000000..4c61929e --- /dev/null +++ b/Editor/RemoveVertexColorPass.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: a227da6f9f1548c3867b1ed113f28e9d +timeCreated: 1733008734 \ No newline at end of file diff --git a/Runtime/RemoveVertexColor.cs b/Runtime/RemoveVertexColor.cs new file mode 100644 index 00000000..d8201ad4 --- /dev/null +++ b/Runtime/RemoveVertexColor.cs @@ -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; + } +} \ No newline at end of file diff --git a/Runtime/RemoveVertexColor.cs.meta b/Runtime/RemoveVertexColor.cs.meta new file mode 100644 index 00000000..d2582877 --- /dev/null +++ b/Runtime/RemoveVertexColor.cs.meta @@ -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: diff --git a/docs~/docs/reference/remove-vertex-color-after.png b/docs~/docs/reference/remove-vertex-color-after.png new file mode 100644 index 00000000..957e1b65 Binary files /dev/null and b/docs~/docs/reference/remove-vertex-color-after.png differ diff --git a/docs~/docs/reference/remove-vertex-color-before.png b/docs~/docs/reference/remove-vertex-color-before.png new file mode 100644 index 00000000..f0e0c1c8 Binary files /dev/null and b/docs~/docs/reference/remove-vertex-color-before.png differ diff --git a/docs~/docs/reference/remove-vertex-color.md b/docs~/docs/reference/remove-vertex-color.md new file mode 100644 index 00000000..5724c9e2 --- /dev/null +++ b/docs~/docs/reference/remove-vertex-color.md @@ -0,0 +1,34 @@ +# Remove Vertex Color + +![Remove Vertex Color](remove-vertex-color.png) + +The Remove Vertex Color component removes vertex colors from the object it is attached to and its children. + +## When should I use it? + +Sometimes, models come with vertex colors that aren't intended for display. When changing to a shader that +makes use of vertex colors, such as the VRChat mobile shaders, this can result in undesired discoloration. You can use +this component to remove these vertex colors nondestructively. + +
+
+
+ ![With unwanted vertex colors](remove-vertex-color-before.png) +
+ *Without Remove Vertex Color, some unwanted vertex colors discolor this avatar's hair.* +
+
+
+ ![After removing vertex colors](remove-vertex-color-after.png) +
+ *After adding Remove Vertex Color, the avatar's hair is the correct color.* +
+
+ +## Detailed usage + +Simply attach the Remove Vertex Color component to an object in your avatar - often, you can just add it to the root +object. All objects below that object in the hierarchy will have their vertex colors removed. + +If you want to exclude some objects, add a Remove Vertex Color component to the object you want to exclude and set +the mode to "Keep Vertex Colors". Any objects below this object will not have their vertex colors removed. \ No newline at end of file diff --git a/docs~/docs/reference/remove-vertex-color.png b/docs~/docs/reference/remove-vertex-color.png new file mode 100644 index 00000000..0b503932 Binary files /dev/null and b/docs~/docs/reference/remove-vertex-color.png differ diff --git a/docs~/i18n/ja/docusaurus-plugin-content-docs/current/reference/remove-vertex-color-after.png b/docs~/i18n/ja/docusaurus-plugin-content-docs/current/reference/remove-vertex-color-after.png new file mode 100644 index 00000000..957e1b65 Binary files /dev/null and b/docs~/i18n/ja/docusaurus-plugin-content-docs/current/reference/remove-vertex-color-after.png differ diff --git a/docs~/i18n/ja/docusaurus-plugin-content-docs/current/reference/remove-vertex-color-before.png b/docs~/i18n/ja/docusaurus-plugin-content-docs/current/reference/remove-vertex-color-before.png new file mode 100644 index 00000000..f0e0c1c8 Binary files /dev/null and b/docs~/i18n/ja/docusaurus-plugin-content-docs/current/reference/remove-vertex-color-before.png differ diff --git a/docs~/i18n/ja/docusaurus-plugin-content-docs/current/reference/remove-vertex-color.md b/docs~/i18n/ja/docusaurus-plugin-content-docs/current/reference/remove-vertex-color.md new file mode 100644 index 00000000..3320746b --- /dev/null +++ b/docs~/i18n/ja/docusaurus-plugin-content-docs/current/reference/remove-vertex-color.md @@ -0,0 +1,33 @@ +# Remove Vertex Color + +![Remove Vertex Color](remove-vertex-color.png) + +Remove Vertex Color コンポーネントは、アタッチされたオブジェクトとその子オブジェクトから頂点カラーを削除します。 + +## いつ使うものか? + +時々、アバターや衣装には意図されていない頂点カラーが付いていることがあります。VRChat Mobile系統など頂点カラーを使用するシェーダーに変更すると、 +変色してしまうことがあります。このコンポーネントを使えば、非破壊的に問題の頂点カラーを削除できます。 + +
+
+
+ ![不要な頂点カラーがある場合](remove-vertex-color-before.png) +
+ *Remove Vertex Color を使わないと、このアバターの髪の毛に不要な頂点カラーで変色してしまいます。* +
+
+
+ ![頂点カラーを削除した後](remove-vertex-color-after.png) +
+ *Remove Vertex Color を追加した後、アバターの髪の色が正しくなります。* +
+
+ +## 詳細な使い方 + +Remove Vertex Color コンポーネントをアバターのオブジェクトに追加してください。通常、ルートオブジェクトに追加するだけで十分です。 +このオブジェクト以下のすべてのオブジェクトの頂点カラーが削除されます。 + +特定のオブジェクトを除外したい場合は、除外したいオブジェクトに Remove Vertex Color コンポーネントを追加し、モードを「頂点カラーを削除しない」 +に設定してください。このオブジェクト以下のオブジェクトの頂点カラーは削除されません。 diff --git a/docs~/i18n/ja/docusaurus-plugin-content-docs/current/reference/remove-vertex-color.png b/docs~/i18n/ja/docusaurus-plugin-content-docs/current/reference/remove-vertex-color.png new file mode 100644 index 00000000..486671f6 Binary files /dev/null and b/docs~/i18n/ja/docusaurus-plugin-content-docs/current/reference/remove-vertex-color.png differ