using System.Collections.Generic; 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() { bool didWork = false; foreach (var target in _avatar.GetComponentsInChildren(true)) { var w = Process(target); didWork = didWork || w; } if (didWork) { // Process meshes foreach (var smr in _avatar.GetComponentsInChildren(true)) { new VisibleHeadAccessoryMeshProcessor(smr, _visibleBones, _proxyHead).Retarget(); } } } bool Process(ModularAvatarVisibleHeadAccessory target) { bool didWork = false; if (Validate(target) == ReadyStatus.Ready) { var proxy = CreateProxy(); 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); var oldPath = RuntimeUtil.AvatarRootPath(target.gameObject); target.transform.SetParent(proxy, true); var newPath = RuntimeUtil.AvatarRootPath(target.gameObject); PathMappings.Remap(oldPath, new PathMappings.MappingEntry() { path = newPath, transformPath = newPath }); 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; 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; 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; } } }