feat: add Mesh Settings component

This commit is contained in:
bd_ 2023-06-05 20:18:46 +09:00
parent 095c2667a0
commit 3c7634e4ea
15 changed files with 388 additions and 5482 deletions

View File

@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: c217d78c5408b04489548a5823bed3d4
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,29 +0,0 @@
using nadena.dev.modular_avatar.core.editor;
using NUnit.Framework;
using UnityEditor;
using UnityEngine;
namespace modular_avatar_tests
{
internal class ProbeAnchorTests : TestBase
{
[Test]
public void TestProbeAnchor()
{
var prefab = CreatePrefab("ProbeAnchorTests.prefab");
AvatarProcessor.ProcessAvatar(prefab);
var root = prefab.transform.Find("RendererRoot");
var target = prefab.transform.Find("ProbeTarget");
var obj1 = prefab.transform.Find("RendererRoot/SkinnedMeshRenderer").GetComponent<Renderer>();
var obj2 = prefab.transform.Find("RendererRoot/MeshRenderer").GetComponent<Renderer>();
var obj3 = prefab.transform.Find("RendererRoot/ParticleSystemRenderer").GetComponent<Renderer>();
var obj4 = prefab.transform.Find("RendererRoot/TrailRenderer").GetComponent<Renderer>();
Assert.AreEqual(target, obj1.probeAnchor);
Assert.AreEqual(target, obj2.probeAnchor);
Assert.AreEqual(target, obj3.probeAnchor);
Assert.AreEqual(target, obj4.probeAnchor);
}
}
}

View File

@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: 0f3adfcc27e73a249badd8f42ca5a57c
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -201,6 +201,7 @@ namespace nadena.dev.modular_avatar.core.editor
new ProbeAnchorProcessor().OnPreprocessAvatar(avatarGameObject);
new VisibleHeadAccessoryProcessor(vrcAvatarDescriptor).Process(context);
new BoundsOverrideProcessor().OnProcessAvatar(avatarGameObject);
new MeshSettingsPass(context).OnPreprocessAvatar();
new RemapAnimationPass(vrcAvatarDescriptor).Process(context.AnimationDatabase);
new BlendshapeSyncAnimationProcessor().OnPreprocessAvatar(avatarGameObject, context);
PhysboneBlockerPass.Process(avatarGameObject);

View File

@ -17,6 +17,7 @@ namespace nadena.dev.modular_avatar.core.editor
out var avatarRoot, out var avatarHips, out var outfitHips)
) return;
var outfitRoot = cmd.context as GameObject;
var avatarArmature = avatarHips.transform.parent;
var outfitArmature = outfitHips.transform.parent;
@ -28,6 +29,95 @@ namespace nadena.dev.modular_avatar.core.editor
merge.InferPrefixSuffix();
HeuristicBoneMapper.RenameBonesByHeuristic(merge);
}
if (outfitRoot != null
&& outfitRoot.GetComponent<ModularAvatarMeshSettings>() == null
&& outfitRoot.GetComponentInParent<ModularAvatarMeshSettings>() == null)
{
var meshSettings = Undo.AddComponent<ModularAvatarMeshSettings>(outfitRoot.gameObject);
Transform rootBone = null, probeAnchor = null;
Bounds bounds = ModularAvatarMeshSettings.DEFAULT_BOUNDS;
FindConsistentSettings(avatarRoot, ref probeAnchor, ref rootBone, ref bounds);
if (probeAnchor == null)
{
probeAnchor = avatarHips.transform;
}
if (rootBone == null)
{
rootBone = avatarRoot.transform;
}
meshSettings.InheritProbeAnchor = ModularAvatarMeshSettings.InheritMode.Set;
meshSettings.InheritBounds = ModularAvatarMeshSettings.InheritMode.Set;
meshSettings.ProbeAnchor = new AvatarObjectReference();
meshSettings.ProbeAnchor.referencePath = RuntimeUtil.RelativePath(avatarRoot, probeAnchor.gameObject);
meshSettings.RootBone = new AvatarObjectReference();
meshSettings.RootBone.referencePath = RuntimeUtil.RelativePath(avatarRoot, rootBone.gameObject);
meshSettings.Bounds = bounds;
}
}
private static void FindConsistentSettings(
GameObject avatarRoot,
ref Transform probeAnchor,
ref Transform rootBone,
ref Bounds bounds
)
{
// We assume the renderers directly under the avatar root came from the original avatar and are _probably_
// set consistently. If so, we use this as a basis for the new outfit's settings.
bool firstRenderer = true;
bool firstSkinnedMeshRenderer = true;
foreach (Transform directChild in avatarRoot.transform)
{
var renderer = directChild.GetComponent<Renderer>();
if (renderer == null) continue;
if (firstRenderer)
{
probeAnchor = renderer.probeAnchor;
}
else
{
if (renderer.probeAnchor != probeAnchor)
{
probeAnchor = null; // inconsistent configuration
}
}
firstRenderer = false;
var skinnedMeshRenderer = renderer as SkinnedMeshRenderer;
if (skinnedMeshRenderer == null) continue;
if (firstSkinnedMeshRenderer)
{
rootBone = skinnedMeshRenderer.rootBone;
bounds = skinnedMeshRenderer.localBounds;
}
else
{
if (rootBone != skinnedMeshRenderer.rootBone)
{
rootBone = null; // inconsistent configuration
bounds = ModularAvatarMeshSettings.DEFAULT_BOUNDS;
}
else if (Vector3.Distance(bounds.center, skinnedMeshRenderer.bounds.center) > 0.01f
|| Vector3.Distance(bounds.extents, skinnedMeshRenderer.bounds.extents) > 0.01f)
{
bounds = ModularAvatarMeshSettings.DEFAULT_BOUNDS;
}
}
firstSkinnedMeshRenderer = false;
}
}
[MenuItem("GameObject/ModularAvatar/Setup Outfit", true, PRIORITY)]

View File

@ -0,0 +1,122 @@
using System;
using UnityEditor;
using UnityEngine;
using static nadena.dev.modular_avatar.core.editor.Localization;
namespace nadena.dev.modular_avatar.core.editor
{
[CustomPropertyDrawer(typeof(ModularAvatarMeshSettings.InheritMode))]
class MeshSettingsInheritMode : EnumDrawer<ModularAvatarMeshSettings.InheritMode>
{
protected override string localizationPrefix => "mesh_settings.inherit_mode";
}
[CustomEditor(typeof(ModularAvatarMeshSettings))]
[CanEditMultipleObjects]
internal class MeshSettingsEditor : MAEditorBase
{
private SerializedProperty _prop_inherit_probe_anchor;
private SerializedProperty _prop_probe_anchor;
private SerializedProperty _prop_inherit_bounds;
private SerializedProperty _prop_root_bone;
private SerializedProperty _prop_bounds;
private void OnEnable()
{
_prop_inherit_probe_anchor =
serializedObject.FindProperty(nameof(ModularAvatarMeshSettings.InheritProbeAnchor));
_prop_probe_anchor = serializedObject.FindProperty(nameof(ModularAvatarMeshSettings.ProbeAnchor));
_prop_inherit_bounds = serializedObject.FindProperty(nameof(ModularAvatarMeshSettings.InheritBounds));
_prop_root_bone = serializedObject.FindProperty(nameof(ModularAvatarMeshSettings.RootBone));
_prop_bounds = serializedObject.FindProperty(nameof(ModularAvatarMeshSettings.Bounds));
}
protected override void OnInnerInspectorGUI()
{
MeshSettingsPass.MergedSettings merged = new MeshSettingsPass.MergedSettings();
bool haveMerged = false;
ModularAvatarMeshSettings settings = null;
if (targets.Length == 1)
{
settings = (ModularAvatarMeshSettings) target;
var avatar = RuntimeUtil.FindAvatarInParents(settings.transform);
if (avatar != null)
{
Component mesh = (Component) target;
merged = MeshSettingsPass.MergeSettings(avatar.transform, mesh.transform);
haveMerged = true;
}
}
serializedObject.Update();
EditorGUILayout.LabelField(G("mesh_settings.header_probe_anchor"), EditorStyles.boldLabel);
EditorGUILayout.PropertyField(_prop_inherit_probe_anchor, G("mesh_settings.inherit_probe_anchor"));
if (_prop_inherit_probe_anchor.enumValueIndex == (int) ModularAvatarMeshSettings.InheritMode.Set)
{
EditorGUILayout.PropertyField(_prop_probe_anchor, G("mesh_settings.probe_anchor"));
}
else if (_prop_inherit_probe_anchor.enumValueIndex == (int) ModularAvatarMeshSettings.InheritMode.Inherit &&
haveMerged)
{
using (new EditorGUI.DisabledScope(true))
{
EditorGUILayout.ObjectField(G("mesh_settings.probe_anchor"), merged.ProbeAnchor, typeof(Transform),
true);
}
}
EditorGUILayout.Separator();
EditorGUILayout.LabelField(G("mesh_settings.header_bounds"), EditorStyles.boldLabel);
EditorGUILayout.PropertyField(_prop_inherit_bounds, G("mesh_settings.inherit_bounds"));
if (_prop_inherit_bounds.enumValueIndex == (int) ModularAvatarMeshSettings.InheritMode.Set)
{
EditorGUILayout.PropertyField(_prop_root_bone, G("mesh_settings.root_bone"));
EditorGUILayout.PropertyField(_prop_bounds, G("mesh_settings.bounds"));
}
else if (_prop_inherit_bounds.enumValueIndex == (int) ModularAvatarMeshSettings.InheritMode.Inherit &&
haveMerged)
{
using (new EditorGUI.DisabledScope(true))
{
EditorGUILayout.ObjectField(G("mesh_settings.root_bone"), merged.RootBone, typeof(Transform), true);
EditorGUILayout.PropertyField(_prop_bounds, G("mesh_settings.bounds"));
}
}
serializedObject.ApplyModifiedProperties();
ShowLanguageUI();
}
[DrawGizmo(GizmoType.Selected)]
private static void DrawGizmo(ModularAvatarMeshSettings component, GizmoType gizmoType)
{
if (component.InheritBounds != ModularAvatarMeshSettings.InheritMode.Set) return;
Matrix4x4 oldMatrix = Gizmos.matrix;
Vector3 center = component.Bounds.center;
Vector3 size = component.Bounds.size;
Transform rootBone = component.RootBone.Get(component)?.transform;
try
{
if (rootBone != null)
{
Gizmos.matrix *= rootBone.localToWorldMatrix;
}
Gizmos.DrawWireCube(center, size);
}
finally
{
Gizmos.matrix = oldMatrix;
}
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 441e32d18e0f4971be8f1853a7ceacae
timeCreated: 1685960604

View File

@ -70,6 +70,19 @@
"bounds_override.root_bone": "Root Bone",
"bounds_override.bounds": "Bounds",
"bounds_override_blocker.help": "Within this object will no longer be affected by the parent's BoundsOverride.",
"mesh_settings.header_probe_anchor": "Anchor Override Configuration",
"mesh_settings.inherit_probe_anchor": "Anchor Override Mode",
"mesh_settings.probe_anchor": "Anchor Override",
"mesh_settings.probe_anchor.tooltip": "Sets the anchor override used for lighting calculations for renderers inside this object",
"mesh_settings.header_bounds": "Bounds Override Configuration",
"mesh_settings.inherit_bounds": "Bounds Override Mode",
"mesh_settings.root_bone": "Root Bone",
"mesh_settings.root_bone.tooltip": "The root bone of the mesh. This is used as a reference point from which to calculate the bounds of the mesh.",
"mesh_settings.bounds": "Bounds",
"mesh_settings.bounds.tooltip": "The bounds of the mesh. This is used to determine when rendering can be skipped for an offscreen mesh.",
"mesh_settings.inherit_mode.Inherit": "Inherit",
"mesh_settings.inherit_mode.Set": "Set",
"mesh_settings.inherit_mode.DontSet": "Don't Set (use mesh as-is)",
"pb_blocker.help": "This object will not be affected by PhysBones attached to parents.",
"probe_anchor.help": "Sets the Anchor Override for the inner renderer object.",
"hint.bad_vrcsdk": "Incompatible version of VRCSDK detected.\n\nPlease try upgrading your VRCSDK; if this does not work, check for a newer version of Modular Avatar as well.",

View File

@ -68,6 +68,19 @@
"bounds_override.root_bone": "ルートボーン",
"bounds_override.bounds": "バウンズ",
"bounds_override_blocker.help": "このオブジェクト内は親のBoundsOverrideの影響を受けなくなります。",
"mesh_settings.header_probe_anchor": "Anchor Override 設定",
"mesh_settings.inherit_probe_anchor": "設定モード",
"mesh_settings.probe_anchor": "Anchor Override",
"mesh_settings.probe_anchor.tooltip": "このオブジェクトとその子のレンダラーのAnchorOverrideを設定します。",
"mesh_settings.header_bounds": "Bounds 設定",
"mesh_settings.inherit_bounds": "設定モード",
"mesh_settings.root_bone": "Root Bone",
"mesh_settings.root_bone.tooltip": "このオブジェクトとその子のメッシュで設定されるルートボーン。メッシュのバウンズを計算するための参照点として使用されます。",
"mesh_settings.bounds": "Bounds",
"mesh_settings.bounds.tooltip": "このオブジェクトとその子のメッシュで設定されるバウンズ。画面外のメッシュのレンダリングを省略するかどうかを決定するために使用されます。",
"mesh_settings.inherit_mode.Inherit": "継承",
"mesh_settings.inherit_mode.Set": "設定",
"mesh_settings.inherit_mode.DontSet": "設定しない(メッシュ本体の設定のまま)",
"pb_blocker.help": "このオブジェクトは親のPhysBoneから影響を受けなくなります。",
"probe_anchor.help": "このオブジェクトに含まれるレンダラーのAnchorOverrideを設定します。",
"hint.bad_vrcsdk": "使用中のVRCSDKのバージョンとは互換性がありません。\n\nVRCSDKを更新してみてください。それでもだめでしたら、Modular Avatarにも最新版が出てないかチェックしてください。",

View File

@ -0,0 +1,110 @@
using UnityEngine;
namespace nadena.dev.modular_avatar.core.editor
{
internal static class InheritModeExtension
{
internal static bool NotFinal(this ModularAvatarMeshSettings.InheritMode mode)
{
return mode == ModularAvatarMeshSettings.InheritMode.Inherit;
}
}
internal class MeshSettingsPass
{
private readonly BuildContext context;
public MeshSettingsPass(BuildContext context)
{
this.context = context;
}
public void OnPreprocessAvatar()
{
foreach (var mesh in context.AvatarDescriptor.GetComponentsInChildren<Renderer>(true))
{
ProcessMesh(mesh);
}
}
internal struct MergedSettings
{
public bool SetAnchor, SetBounds;
public Transform ProbeAnchor;
public Transform RootBone;
public Bounds Bounds;
}
private static bool Inherit(ref ModularAvatarMeshSettings.InheritMode mode,
ModularAvatarMeshSettings.InheritMode srcmode)
{
if (mode != ModularAvatarMeshSettings.InheritMode.Inherit ||
srcmode == ModularAvatarMeshSettings.InheritMode.Inherit)
return false;
mode = srcmode;
return true;
}
internal static MergedSettings MergeSettings(Transform avatarRoot, Transform referenceObject)
{
MergedSettings merged = new MergedSettings();
Transform current = referenceObject;
ModularAvatarMeshSettings.InheritMode inheritProbeAnchor = ModularAvatarMeshSettings.InheritMode.Inherit;
ModularAvatarMeshSettings.InheritMode inheritBounds = ModularAvatarMeshSettings.InheritMode.Inherit;
do
{
var settings = current.GetComponent<ModularAvatarMeshSettings>();
if (current == avatarRoot)
{
current = null;
}
else
{
current = current.transform.parent;
}
if (settings == null)
{
continue;
}
if (Inherit(ref inheritProbeAnchor, settings.InheritProbeAnchor))
{
merged.ProbeAnchor = settings.ProbeAnchor.Get(settings)?.transform;
}
if (Inherit(ref inheritBounds, settings.InheritBounds))
{
merged.RootBone = settings.RootBone.Get(settings)?.transform;
merged.Bounds = settings.Bounds;
}
} while (current != null && (inheritProbeAnchor.NotFinal() || inheritBounds.NotFinal()));
merged.SetAnchor = inheritProbeAnchor == ModularAvatarMeshSettings.InheritMode.Set;
merged.SetBounds = inheritBounds == ModularAvatarMeshSettings.InheritMode.Set;
return merged;
}
private void ProcessMesh(Renderer mesh)
{
MergedSettings settings = MergeSettings(context.AvatarDescriptor.transform, mesh.transform);
if (settings.SetAnchor)
{
mesh.probeAnchor = settings.ProbeAnchor;
}
if (settings.SetBounds && mesh is SkinnedMeshRenderer smr)
{
smr.rootBone = settings.RootBone;
smr.localBounds = settings.Bounds;
}
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 06b7e7286f434327b1d0ef4b60496055
timeCreated: 1685960001

View File

@ -15,7 +15,7 @@ namespace nadena.dev.modular_avatar.core
public GameObject Get(Component container)
{
if (_cacheValid && _cachedPath == referencePath) return _cachedReference;
if (_cacheValid && _cachedPath == referencePath && _cachedReference != null) return _cachedReference;
_cacheValid = true;
_cachedPath = referencePath;

View File

@ -0,0 +1,30 @@
using System;
using UnityEngine;
using UnityEngine.Serialization;
namespace nadena.dev.modular_avatar.core
{
[AddComponentMenu("Modular Avatar/MA Mesh Settings")]
[DisallowMultipleComponent]
public class ModularAvatarMeshSettings : AvatarTagComponent
{
internal static readonly Bounds DEFAULT_BOUNDS = new Bounds(Vector3.zero, Vector3.one * 2);
[Serializable]
public enum InheritMode
{
Inherit,
Set,
DontSet
}
//[Header("Probe anchor configuration")]
public InheritMode InheritProbeAnchor = InheritMode.Inherit;
public AvatarObjectReference ProbeAnchor;
//[Header("Bounds configuration")]
public InheritMode InheritBounds = InheritMode.Inherit;
public AvatarObjectReference RootBone;
public Bounds Bounds = DEFAULT_BOUNDS;
}
}

View File

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