using System; using System.Collections.Generic; using ABI_RC.Core; using UnityEngine; using UnityEngine.Rendering; using Object = UnityEngine.Object; namespace NAK.BetterShadowClone; public static class ShadowCloneHelper { public static ComputeShader shader; public static Material shadowMaterial; #region Avatar Setup public static void SetupAvatar(GameObject avatar) { Animator animator = avatar.GetComponent(); if (animator == null || animator.avatar == null || animator.avatar.isHuman == false) { ShadowCloneMod.Logger.Warning("Avatar is not humanoid!"); return; } Transform headBone = animator.GetBoneTransform(HumanBodyBones.Head); if (headBone == null) { ShadowCloneMod.Logger.Warning("Head bone not found!"); return; } var renderers = avatar.GetComponentsInChildren(true); if (renderers == null || renderers.Length == 0) { ShadowCloneMod.Logger.Warning("No renderers found!"); return; } // create shadow clones ProcessRenderers(renderers, avatar.transform, headBone); } private static void ProcessRenderers(IEnumerable renderers, Transform root, Transform headBone) { IReadOnlyDictionary exclusions = CollectTransformToExclusionMap(root, headBone); foreach (Renderer renderer in renderers) { ConfigureRenderer(renderer); if (ModSettings.EntryUseShadowClone.Value) { IShadowClone clone = ShadowCloneManager.CreateShadowClone(renderer); if (clone != null) ShadowCloneManager.Instance.AddShadowClone(clone); } ITransformHider hider = TransformHiderManager.CreateTransformHider(renderer, exclusions); if (hider != null) TransformHiderManager.Instance.AddTransformHider(hider); } } #endregion #region FPR Exclusion Processing private static Dictionary CollectTransformToExclusionMap(Component root, Transform headBone) { // add an fpr exclusion to the head bone headBone.gameObject.AddComponent().target = headBone; // get all FPRExclusions var fprExclusions = root.GetComponentsInChildren(true).ToList(); // get all valid exclusion targets, and destroy invalid exclusions Dictionary exclusionTargets = new(); for (int i = fprExclusions.Count - 1; i >= 0; i--) { FPRExclusion exclusion = fprExclusions[i]; if (exclusion.target == null) { Object.Destroy(exclusion); continue; } exclusionTargets.Add(exclusion.target, exclusion); } // process each FPRExclusion (recursive) foreach (FPRExclusion exclusion in fprExclusions) ProcessExclusion(exclusion, exclusion.target); // log totals ShadowCloneMod.Logger.Msg($"Exclusions: {fprExclusions.Count}"); return exclusionTargets; void ProcessExclusion(FPRExclusion exclusion, Transform transform) { if (exclusionTargets.ContainsKey(transform) && exclusionTargets[transform] != exclusion) return; // found other exclusion root exclusion.affectedChildren.Add(transform); // associate with the exclusion exclusionTargets.Add(transform, exclusion); // add to the list (yes theres duplicates) foreach (Transform child in transform) ProcessExclusion(exclusion, child); // process children } } #endregion #region Generic Renderer Configuration internal static void ConfigureRenderer(Renderer renderer, bool isShadowClone = false) { // generic optimizations renderer.motionVectorGenerationMode = MotionVectorGenerationMode.ForceNoMotion; // don't let visual/shadow mesh cull in weird worlds renderer.allowOcclusionWhenDynamic = false; // (third person stripped local player naked when camera was slightly occluded) // shadow clone optimizations (always MeshRenderer) if (isShadowClone) { renderer.receiveShadows = false; renderer.lightProbeUsage = LightProbeUsage.Off; renderer.reflectionProbeUsage = ReflectionProbeUsage.Off; return; } if (renderer is not SkinnedMeshRenderer skinnedMeshRenderer) return; // GraphicsBuffer becomes stale randomly otherwise ??? //skinnedMeshRenderer.updateWhenOffscreen = true; // skin mesh renderer optimizations skinnedMeshRenderer.skinnedMotionVectors = false; skinnedMeshRenderer.forceMatrixRecalculationPerRender = false; // expensive skinnedMeshRenderer.quality = SkinQuality.Bone4; } #endregion }