using System.Collections.Generic; using nadena.dev.modular_avatar.editor.ErrorReporting; using UnityEngine; using UnityEngine.Animations; using VRC.SDK3.Avatars.Components; using VRC.SDK3.Dynamics.PhysBone.Components; namespace nadena.dev.modular_avatar.core.editor { internal class VisibleHeadAccessoryProcessor { private const double EPSILON = 0.01; internal enum ReadyStatus { Ready, ParentMarked, NotUnderHead, InPhysBoneChain } private VRCAvatarDescriptor _avatar; private HashSet _activeBones = new HashSet(); private Transform _headBone; private HashSet _visibleBones = new HashSet(); private Transform _proxyHead; public VisibleHeadAccessoryProcessor(VRCAvatarDescriptor avatar) { _avatar = avatar; var animator = avatar.GetComponent(); _headBone = animator != null ? animator.GetBoneTransform(HumanBodyBones.Head) : null; foreach (var physBone in avatar.GetComponentsInChildren(true)) { var boneRoot = physBone.rootTransform != null ? physBone.rootTransform : physBone.transform; var ignored = new HashSet(physBone.ignoreTransforms); foreach (Transform child in boneRoot) { Traverse(child, ignored); } } void Traverse(Transform bone, HashSet ignored) { if (ignored.Contains(bone)) return; _activeBones.Add(bone); foreach (Transform child in bone) { Traverse(child, ignored); } } } public void Process(BuildContext context) { bool didWork = false; foreach (var target in _avatar.GetComponentsInChildren(true)) { var w = BuildReport.ReportingObject(target, () => Process(target)); didWork = didWork || w; } if (didWork) { // Process meshes foreach (var smr in _avatar.GetComponentsInChildren(true)) { BuildReport.ReportingObject(smr, () => new VisibleHeadAccessoryMeshProcessor(smr, _visibleBones, _proxyHead).Retarget(context)); } } } bool Process(ModularAvatarVisibleHeadAccessory target) { bool didWork = false; if (Validate(target) == ReadyStatus.Ready) { var proxy = CreateProxy(); target.transform.SetParent(proxy, true); didWork = true; } if (didWork) { foreach (var xform in target.GetComponentsInChildren(true)) { _visibleBones.Add(xform); } } Object.DestroyImmediate(target); return didWork; } private Transform CreateProxy() { if (_proxyHead != null) return _proxyHead; var src = _headBone; 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; Debug.Log($"src.localScale = {src.localScale} obj.transform.localScale = {obj.transform.localScale}"); var constraint = obj.AddComponent(); 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}; _proxyHead = obj.transform; // TODO - lock proxy scale to head scale in animation? return obj.transform; } internal ReadyStatus Validate(ModularAvatarVisibleHeadAccessory target) { ReadyStatus status = ReadyStatus.NotUnderHead; Transform node = target.transform.parent; if (_activeBones.Contains(target.transform)) return ReadyStatus.InPhysBoneChain; while (node != null) { if (node.GetComponent()) return ReadyStatus.ParentMarked; if (_activeBones.Contains(node)) return ReadyStatus.InPhysBoneChain; if (node == _headBone) { status = ReadyStatus.Ready; break; } node = node.parent; } return status; } } }