Initial implementation of the 1p-visibility trait component

This commit is contained in:
bd_ 2022-11-06 17:03:06 -08:00 committed by bd_
parent 578e7a565c
commit 1ce110cb08
9 changed files with 215 additions and 3 deletions

View File

@ -109,12 +109,12 @@ namespace net.fushizen.modular_avatar.core.editor
new RenameParametersHook().OnPreprocessAvatar(avatarGameObject);
new MenuInstallHook().OnPreprocessAvatar(avatarGameObject);
new MergeArmatureHook().OnPreprocessAvatar(avatarGameObject);
new RetargetMeshes().OnPreprocessAvatar(avatarGameObject);
new BoneProxyProcessor().OnPreprocessAvatar(avatarGameObject);
new MergeAnimatorProcessor().OnPreprocessAvatar(avatarGameObject);
new BlendshapeSyncAnimationProcessor().OnPreprocessAvatar(avatarGameObject);
new FirstPersonVisibleProcessor(avatarGameObject.GetComponent<VRCAvatarDescriptor>()).Process();
AfterProcessing?.Invoke(avatarGameObject);

View File

@ -0,0 +1,135 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Animations;
using VRC.SDK3.Avatars.Components;
using VRC.SDK3.Dynamics.PhysBone.Components;
namespace net.fushizen.modular_avatar.core.editor
{
internal class FirstPersonVisibleProcessor
{
private const double EPSILON = 0.01;
internal enum ReadyStatus
{
Ready,
ParentMarked,
NotUnderHead,
InPhysBoneChain
}
private VRCAvatarDescriptor _avatar;
private HashSet<Transform> _activeBones = new HashSet<Transform>();
private Transform _headBone;
private Dictionary<Transform, Transform> _proxyBones = new Dictionary<Transform, Transform>();
public FirstPersonVisibleProcessor(VRCAvatarDescriptor avatar)
{
_avatar = avatar;
var animator = avatar.GetComponent<Animator>();
_headBone = animator != null ? animator.GetBoneTransform(HumanBodyBones.Head) : null;
foreach (var physBone in avatar.GetComponentsInChildren<VRCPhysBone>(true))
{
var boneRoot = physBone.rootTransform != null ? physBone.rootTransform : physBone.transform;
var ignored = new HashSet<Transform>(physBone.ignoreTransforms);
foreach (Transform child in boneRoot)
{
Traverse(child, ignored);
}
}
void Traverse(Transform bone, HashSet<Transform> ignored)
{
if (ignored.Contains(bone)) return;
_activeBones.Add(bone);
foreach (Transform child in bone)
{
Traverse(child, ignored);
}
}
}
public void Process()
{
foreach (var target in _avatar.GetComponentsInChildren<ModularAvatarFirstPersonVisible>(true))
{
Process(target);
}
}
void Process(ModularAvatarFirstPersonVisible target)
{
if (Validate(target) == ReadyStatus.Ready)
{
var proxy = CreateProxy(_headBone);
var xform = target.transform;
var pscale = proxy.lossyScale;
var oscale = xform.lossyScale;
xform.localScale = new Vector3(oscale.x / pscale.x, oscale.y / pscale.y, oscale.z / pscale.z);
target.transform.SetParent(proxy, true);
}
Object.DestroyImmediate(target);
}
private Transform CreateProxy(Transform src)
{
if (_proxyBones.TryGetValue(src, out var proxy)) return proxy;
GameObject obj = new GameObject(src.name + " (FirstPersonVisible)");
Transform parent = _headBone.parent;
obj.transform.SetParent(parent, false);
obj.transform.localPosition = src.localPosition;
obj.transform.localRotation = src.localRotation;
obj.transform.localScale = src.localScale;
var constraint = obj.AddComponent<ParentConstraint>();
constraint.AddSource(new ConstraintSource()
{
weight = 1.0f,
sourceTransform = src
});
constraint.constraintActive = true;
constraint.locked = true;
constraint.rotationOffsets = new[] {Vector3.zero};
constraint.translationOffsets = new[] {Vector3.zero};
_proxyBones.Add(src, obj.transform);
return obj.transform;
}
internal ReadyStatus Validate(ModularAvatarFirstPersonVisible target)
{
ReadyStatus status = ReadyStatus.NotUnderHead;
Transform node = target.transform.parent;
if (_activeBones.Contains(target.transform)) return ReadyStatus.InPhysBoneChain;
while (node != null)
{
if (node.GetComponent<ModularAvatarFirstPersonVisible>()) return ReadyStatus.ParentMarked;
if (_activeBones.Contains(node)) return ReadyStatus.InPhysBoneChain;
if (node == _headBone)
{
status = ReadyStatus.Ready;
break;
}
node = node.parent;
}
return status;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 0466a62b8e2b4dd39117b650dcd1c312
timeCreated: 1667775370

View File

@ -0,0 +1,53 @@
using System;
using UnityEditor;
using UnityEngine;
namespace net.fushizen.modular_avatar.core.editor
{
[CustomEditor(typeof(ModularAvatarFirstPersonVisible))]
public class FirstPersonVisibleEditor : Editor
{
private FirstPersonVisibleProcessor _processor;
private void OnEnable()
{
var target = (ModularAvatarFirstPersonVisible) this.target;
var avatar = RuntimeUtil.FindAvatarInParents(target.transform);
if (avatar != null) _processor = new FirstPersonVisibleProcessor(avatar);
}
public override void OnInspectorGUI()
{
var target = (ModularAvatarFirstPersonVisible) this.target;
#if UNITY_ANDROID
EditorGUILayout.HelpBox(Localization.S("fpvisible.quest"), MessageType.Warning);
#else
if (_processor != null)
{
var status = _processor.Validate(target);
switch (status)
{
case FirstPersonVisibleProcessor.ReadyStatus.Ready:
case FirstPersonVisibleProcessor.ReadyStatus.ParentMarked:
EditorGUILayout.HelpBox(Localization.S("fpvisible.normal"), MessageType.Info);
break;
default:
{
var label = "fpvisible." + status;
EditorGUILayout.HelpBox(Localization.S(label), MessageType.Error);
break;
}
}
}
#endif
Localization.ShowLanguageUI();
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: b478fb92b39b4df58f10aef1db3b2ffd
timeCreated: 1667774854

View File

@ -37,5 +37,9 @@
"merge_animator.path_mode": "Path Mode",
"merge_animator.path_mode.tooltip": "How to interpret paths in animations. Using relative mode lets you record animations from an animator on this object.",
"merge_animator.match_avatar_write_defaults": "Match Avatar Write Defaults",
"merge_animator.match_avatar_write_defaults.tooltip": "Match the write defaults setting used on the avatar's animator. If the avatar's write defaults settings are inconsistent, the settings on the animator will be left alone."
"merge_animator.match_avatar_write_defaults.tooltip": "Match the write defaults setting used on the avatar's animator. If the avatar's write defaults settings are inconsistent, the settings on the animator will be left alone.",
"fpvisible.normal": "This object will be visible in your first person view.",
"fpvisible.NotUnderHead": "This component has no effect when not placed under the head bone.",
"fpvisible.quest": "This component is not compatible with the standalone Oculus Quest and will have no effect.",
"fpvisible.InPhysBoneChain": "This object is controlled by a Physics Bone chain and cannot be made visible in first person safely. Select the start of the chain instead."
}

View File

@ -37,5 +37,9 @@
"merge_animator.path_mode": "パースモード",
"merge_animator.path_mode.tooltip": "アニメーション内のパースを解釈するモード。相対的にすると、このオブジェクトについているアニメーターでアニメーション編集できます",
"merge_animator.match_avatar_write_defaults": "アバターのWrite Defaults設定に合わせる",
"merge_animator.match_avatar_write_defaults.tooltip": "アバターの該当アニメーターのWrite Defaults設定に合わせます。アバター側の設定が矛盾する場合は、統合されるアニメーターのWD値がそのまま採用されます。"
"merge_animator.match_avatar_write_defaults.tooltip": "アバターの該当アニメーターのWrite Defaults設定に合わせます。アバター側の設定が矛盾する場合は、統合されるアニメーターのWD値がそのまま採用されます。",
"fpvisible.normal": "このオブジェクトは一人視点で表示されます。",
"fpvisible.quest": "このコンポーネントはクエスト単体非対応のため無効化となっています。",
"fpvisible.NotUnderHead": "このコンポーネントはヘッドボーン外では効果がありません。",
"fpvisible.InPhysBoneChain": "このオブジェクトはPhysBoneに制御されているため、一人視点で表示できません。PhysBoneの始点を指定してください。"
}

View File

@ -0,0 +1,7 @@
namespace net.fushizen.modular_avatar.core
{
public class ModularAvatarFirstPersonVisible : AvatarTagComponent
{
// no configuration needed
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 33dac8cfeaeb4c399ddd90597f849f70
timeCreated: 1667774788