From 3660b8f68349f7f7cd4beff1148c80c2fd017791 Mon Sep 17 00:00:00 2001 From: NotAKidoS <37721153+NotAKidoS@users.noreply.github.com> Date: Wed, 29 Jan 2025 16:37:15 -0600 Subject: [PATCH] AvatarCloneTest: push so i can reference in an email --- .../AvatarClone/AvatarClone.API.cs | 68 +++++++ .../AvatarClone/AvatarClone.Clones.cs | 103 ++++++++++ .../AvatarClone/AvatarClone.Exclusion.cs | 141 ++++++++++++++ .../AvatarClone/AvatarClone.Fields.cs | 67 +++++++ .../AvatarClone/AvatarClone.Init.cs | 128 +++++++++++++ .../AvatarClone/AvatarClone.MagicaSupport.cs | 34 ++++ .../AvatarClone/AvatarClone.Update.cs | 181 ++++++++++++++++++ .../AvatarClone/AvatarClone.Util.cs | 20 ++ AvatarCloneTest/AvatarClone/AvatarClone.cs | 134 +++++++++++++ .../FPRExclusion/AvatarCloneExclusion.cs | 38 ++++ AvatarCloneTest/AvatarCloneTest.csproj | 9 + AvatarCloneTest/Main.cs | 72 +++++++ AvatarCloneTest/Patches.cs | 87 +++++++++ AvatarCloneTest/Properties/AssemblyInfo.cs | 32 ++++ AvatarCloneTest/README.md | 18 ++ AvatarCloneTest/format.json | 23 +++ 16 files changed, 1155 insertions(+) create mode 100644 AvatarCloneTest/AvatarClone/AvatarClone.API.cs create mode 100644 AvatarCloneTest/AvatarClone/AvatarClone.Clones.cs create mode 100644 AvatarCloneTest/AvatarClone/AvatarClone.Exclusion.cs create mode 100644 AvatarCloneTest/AvatarClone/AvatarClone.Fields.cs create mode 100644 AvatarCloneTest/AvatarClone/AvatarClone.Init.cs create mode 100644 AvatarCloneTest/AvatarClone/AvatarClone.MagicaSupport.cs create mode 100644 AvatarCloneTest/AvatarClone/AvatarClone.Update.cs create mode 100644 AvatarCloneTest/AvatarClone/AvatarClone.Util.cs create mode 100644 AvatarCloneTest/AvatarClone/AvatarClone.cs create mode 100644 AvatarCloneTest/AvatarClone/FPRExclusion/AvatarCloneExclusion.cs create mode 100644 AvatarCloneTest/AvatarCloneTest.csproj create mode 100644 AvatarCloneTest/Main.cs create mode 100644 AvatarCloneTest/Patches.cs create mode 100644 AvatarCloneTest/Properties/AssemblyInfo.cs create mode 100644 AvatarCloneTest/README.md create mode 100644 AvatarCloneTest/format.json diff --git a/AvatarCloneTest/AvatarClone/AvatarClone.API.cs b/AvatarCloneTest/AvatarClone/AvatarClone.API.cs new file mode 100644 index 0000000..89835bd --- /dev/null +++ b/AvatarCloneTest/AvatarClone/AvatarClone.API.cs @@ -0,0 +1,68 @@ +using UnityEngine; + +namespace NAK.AvatarCloneTest; + +public partial class AvatarClone +{ + #region Public API Methods + + /// + /// Sets whether the specific renderer requires additional runtime checks when copying to the clone. + /// For example, Magica Cloth modifies the sharedMesh & bones of the renderer at runtime. This is not needed + /// for most renderers, so copying for all renderers would be inefficient. + /// + public void SetRendererNeedsAdditionalChecks(Renderer rend, bool needsChecks) + { + switch (rend) + { + case MeshRenderer meshRenderer: + { + int index = _standardRenderers.IndexOf(meshRenderer); + if (index == -1) return; + + if (needsChecks && !_standardRenderersNeedingChecks.Contains(index)) + { + int insertIndex = _standardRenderersNeedingChecks.Count; + _standardRenderersNeedingChecks.Add(index); + _cachedSharedMeshes.Insert(insertIndex, null); + } + else if (!needsChecks) + { + int removeIndex = _standardRenderersNeedingChecks.IndexOf(index); + if (removeIndex != -1) + { + _standardRenderersNeedingChecks.RemoveAt(removeIndex); + _cachedSharedMeshes.RemoveAt(removeIndex); + } + } + return; + } + case SkinnedMeshRenderer skinnedRenderer: + { + int index = _skinnedRenderers.IndexOf(skinnedRenderer); + if (index == -1) return; + + if (needsChecks && !_skinnedRenderersNeedingChecks.Contains(index)) + { + int insertIndex = _skinnedRenderersNeedingChecks.Count; + _skinnedRenderersNeedingChecks.Add(index); + _cachedSharedMeshes.Insert(_standardRenderersNeedingChecks.Count + insertIndex, null); + _cachedSkinnedBoneCounts.Add(0); + } + else if (!needsChecks) + { + int removeIndex = _skinnedRenderersNeedingChecks.IndexOf(index); + if (removeIndex != -1) + { + _skinnedRenderersNeedingChecks.RemoveAt(removeIndex); + _cachedSharedMeshes.RemoveAt(_standardRenderersNeedingChecks.Count + removeIndex); + _cachedSkinnedBoneCounts.RemoveAt(removeIndex); + } + } + break; + } + } + } + + #endregion Public API Methods +} \ No newline at end of file diff --git a/AvatarCloneTest/AvatarClone/AvatarClone.Clones.cs b/AvatarCloneTest/AvatarClone/AvatarClone.Clones.cs new file mode 100644 index 0000000..8fb54ec --- /dev/null +++ b/AvatarCloneTest/AvatarClone/AvatarClone.Clones.cs @@ -0,0 +1,103 @@ +using ABI_RC.Core; +using UnityEngine; +using UnityEngine.Rendering; + +namespace NAK.AvatarCloneTest; + +public partial class AvatarClone +{ + #region Clone Creation + + private void CreateClones() + { + int standardCount = _standardRenderers.Count; + _standardClones = new List(standardCount); + _standardCloneFilters = new List(standardCount); + for (int i = 0; i < standardCount; i++) CreateStandardClone(i); + + int skinnedCount = _skinnedRenderers.Count; + _skinnedClones = new List(skinnedCount); + for (int i = 0; i < skinnedCount; i++) CreateSkinnedClone(i); + } + + private void CreateStandardClone(int index) + { + MeshRenderer sourceRenderer = _standardRenderers[index]; + MeshFilter sourceFilter = _standardFilters[index]; + + GameObject go = new(sourceRenderer.name + "_VisualClone") + { + layer = CVRLayers.PlayerClone + }; + + go.transform.SetParent(sourceRenderer.transform, false); + //go.transform.SetLocalPositionAndRotation(Vector3.zero, Quaternion.identity); + + MeshRenderer cloneRenderer = go.AddComponent(); + MeshFilter cloneFilter = go.AddComponent(); + + // Initial setup + cloneRenderer.sharedMaterials = sourceRenderer.sharedMaterials; + cloneRenderer.shadowCastingMode = ShadowCastingMode.Off; + cloneRenderer.probeAnchor = sourceRenderer.probeAnchor; + cloneRenderer.localBounds = new Bounds(Vector3.zero, Vector3.positiveInfinity); + cloneFilter.sharedMesh = sourceFilter.sharedMesh; + + // Optimizations to enforce + cloneRenderer.motionVectorGenerationMode = MotionVectorGenerationMode.ForceNoMotion; + cloneRenderer.allowOcclusionWhenDynamic = false; + + // Optimizations to enforce + sourceRenderer.motionVectorGenerationMode = MotionVectorGenerationMode.ForceNoMotion; + sourceRenderer.allowOcclusionWhenDynamic = false; + + _standardClones.Add(cloneRenderer); + _standardCloneFilters.Add(cloneFilter); + } + + private void CreateSkinnedClone(int index) + { + SkinnedMeshRenderer source = _skinnedRenderers[index]; + + GameObject go = new(source.name + "_VisualClone") + { + layer = CVRLayers.PlayerClone + }; + + go.transform.SetParent(source.transform, false); + //go.transform.SetLocalPositionAndRotation(Vector3.zero, Quaternion.identity); + + SkinnedMeshRenderer clone = go.AddComponent(); + + // Initial setup + clone.sharedMaterials = source.sharedMaterials; + clone.shadowCastingMode = ShadowCastingMode.Off; + clone.probeAnchor = source.probeAnchor; + clone.localBounds = new Bounds(Vector3.zero, Vector3.positiveInfinity); + clone.sharedMesh = source.sharedMesh; + clone.rootBone = source.rootBone; + clone.bones = source.bones; + clone.quality = source.quality; + clone.updateWhenOffscreen = source.updateWhenOffscreen; + + // Optimizations to enforce + clone.motionVectorGenerationMode = MotionVectorGenerationMode.ForceNoMotion; + clone.allowOcclusionWhenDynamic = false; + clone.updateWhenOffscreen = false; + clone.skinnedMotionVectors = false; + clone.forceMatrixRecalculationPerRender = false; + clone.quality = SkinQuality.Bone4; + + // Optimizations to enforce + source.motionVectorGenerationMode = MotionVectorGenerationMode.ForceNoMotion; + source.allowOcclusionWhenDynamic = false; + source.updateWhenOffscreen = false; + source.skinnedMotionVectors = false; + source.forceMatrixRecalculationPerRender = false; + source.quality = SkinQuality.Bone4; + + _skinnedClones.Add(clone); + } + + #endregion Clone Creation +} \ No newline at end of file diff --git a/AvatarCloneTest/AvatarClone/AvatarClone.Exclusion.cs b/AvatarCloneTest/AvatarClone/AvatarClone.Exclusion.cs new file mode 100644 index 0000000..d5136e3 --- /dev/null +++ b/AvatarCloneTest/AvatarClone/AvatarClone.Exclusion.cs @@ -0,0 +1,141 @@ +using ABI.CCK.Components; +using UnityEngine; + +namespace NAK.AvatarCloneTest; + +public partial class AvatarClone +{ + private readonly Dictionary> _exclusionDirectRenderers = new(); + private readonly Dictionary> _exclusionControlledBones = new(); + + private void InitializeExclusions() + { + // Add head exclusion for humanoid avatars if not present + var animator = GetComponent(); + if (animator != null && animator.isHuman) + { + var headBone = animator.GetBoneTransform(HumanBodyBones.Head); + if (headBone != null && headBone.GetComponent() == null) + { + var exclusion = headBone.gameObject.AddComponent(); + exclusion.isShown = false; + exclusion.target = headBone; + exclusion.shrinkToZero = true; + } + } + + // Process existing exclusions bottom-up + var exclusions = GetComponentsInChildren(true); + + for (int i = exclusions.Length - 1; i >= 0; i--) + { + var exclusion = exclusions[i]; + if (exclusion.target == null) + exclusion.target = exclusion.transform; + + // Skip invalid exclusions or already processed targets + if (exclusion.target == null || _exclusionDirectRenderers.ContainsKey(exclusion.target)) + { + Destroy(exclusion); + continue; + } + + // Initialize data for this exclusion + _exclusionDirectRenderers[exclusion.target] = new HashSet(); + _exclusionControlledBones[exclusion.target] = new HashSet(); + + // Set up our behaviour + exclusion.behaviour = new AvatarCloneExclusion(this, exclusion.target); + + // Collect affected renderers and bones + CollectExclusionData(exclusion.target); + + // Initial update + exclusion.UpdateExclusions(); + } + } + + private void CollectExclusionData(Transform target) + { + var stack = new Stack(); + stack.Push(target); + + while (stack.Count > 0) + { + var current = stack.Pop(); + + // Skip if this transform belongs to another exclusion + if (current != target && current.GetComponent() != null) + continue; + + _exclusionControlledBones[target].Add(current); + + // Add renderers that will need their clone visibility toggled + foreach (var renderer in current.GetComponents()) + { + // Find corresponding clone renderer + if (renderer is MeshRenderer meshRenderer) + { + int index = _standardRenderers.IndexOf(meshRenderer); + if (index != -1) + _exclusionDirectRenderers[target].Add(_standardClones[index]); + } + else if (renderer is SkinnedMeshRenderer skinnedRenderer) + { + int index = _skinnedRenderers.IndexOf(skinnedRenderer); + if (index != -1) + _exclusionDirectRenderers[target].Add(_skinnedClones[index]); + } + } + + // Add children to stack + foreach (Transform child in current) + { + stack.Push(child); + } + } + } + + public void HandleExclusionUpdate(Transform target, Transform shrinkBone, bool isShown) + { + if (!_exclusionDirectRenderers.TryGetValue(target, out var directCloneRenderers) || + !_exclusionControlledBones.TryGetValue(target, out var controlledBones)) + return; + + // Handle direct clone renderers + foreach (var cloneRenderer in directCloneRenderers) + { + cloneRenderer.enabled = isShown; + } + + // Update bone references in clone renderers + int cloneCount = _skinnedClones.Count; + var cloneRenderers = _skinnedClones; + var sourceRenderers = _skinnedRenderers; + + for (int i = 0; i < cloneCount; i++) + { + var clone = cloneRenderers[i]; + var source = sourceRenderers[i]; + var sourceBones = source.bones; + var cloneBones = clone.bones; + int boneCount = cloneBones.Length; + bool needsUpdate = false; + + for (int j = 0; j < boneCount; j++) + { + // Check if this bone is in our controlled set + if (controlledBones.Contains(sourceBones[j])) + { + cloneBones[j] = isShown ? sourceBones[j] : shrinkBone; + needsUpdate = true; + } + } + + if (needsUpdate) + { + clone.bones = cloneBones; + } + } + } +} \ No newline at end of file diff --git a/AvatarCloneTest/AvatarClone/AvatarClone.Fields.cs b/AvatarCloneTest/AvatarClone/AvatarClone.Fields.cs new file mode 100644 index 0000000..0ba69b0 --- /dev/null +++ b/AvatarCloneTest/AvatarClone/AvatarClone.Fields.cs @@ -0,0 +1,67 @@ +using UnityEngine; + +namespace NAK.AvatarCloneTest; + +public partial class AvatarClone +{ + #region Profile Markers +//#if UNITY_EDITOR + private static readonly UnityEngine.Profiling.CustomSampler s_CopyMaterials = + UnityEngine.Profiling.CustomSampler.Create("AvatarClone2.CopyMaterials"); + private static readonly UnityEngine.Profiling.CustomSampler s_CopyBlendShapes = + UnityEngine.Profiling.CustomSampler.Create("AvatarClone2.CopyBlendShapes"); + private static readonly UnityEngine.Profiling.CustomSampler s_CopyMeshes = + UnityEngine.Profiling.CustomSampler.Create("AvatarClone2.CopyMeshes"); + + private static readonly UnityEngine.Profiling.CustomSampler s_MyOnPreRender = + UnityEngine.Profiling.CustomSampler.Create("AvatarClone2.MyOnPreRender"); + private static readonly UnityEngine.Profiling.CustomSampler s_SetShadowsOnly = + UnityEngine.Profiling.CustomSampler.Create("AvatarClone2.SetShadowsOnly"); + private static readonly UnityEngine.Profiling.CustomSampler s_UndoShadowsOnly = + UnityEngine.Profiling.CustomSampler.Create("AvatarClone2.UndoShadowsOnly"); + private static readonly UnityEngine.Profiling.CustomSampler s_SetUiCulling = + UnityEngine.Profiling.CustomSampler.Create("AvatarClone2.SetUiCulling"); + private static readonly UnityEngine.Profiling.CustomSampler s_UndoUiCulling = + UnityEngine.Profiling.CustomSampler.Create("AvatarClone2.UndoUiCulling"); + +//#endif + #endregion Profile Markers + + #region Source Renderers + private List _standardRenderers; + private List _standardFilters; + private List _skinnedRenderers; + private List _allSourceRenderers; // For shadow casting only + #endregion Source Renderers + + #region Clone Renderers + private List _standardClones; + private List _standardCloneFilters; + private List _skinnedClones; + #endregion Clone Renderers + + #region Dynamic Check Lists + private List _standardRenderersNeedingChecks; // Stores indices into _standardRenderers + private List _skinnedRenderersNeedingChecks; // Stores indices into _skinnedRenderers + private List _cachedSkinnedBoneCounts; // So we don't copy the bones unless they've changed + private List _cachedSharedMeshes; // So we don't copy the mesh unless it's changed + #endregion Dynamic Check Lists + + #region Material Data + private List _localMaterials; + private List _cullingMaterials; + private List _mainMaterials; + private MaterialPropertyBlock _propertyBlock; + #endregion Material Data + + #region Blend Shape Data + private List> _blendShapeWeights; + #endregion Blend Shape Data + + #region Shadow and UI Culling Settings + private bool _uiCullingActive; + private bool _shadowsOnlyActive; + private bool[] _originallyHadShadows; + private bool[] _originallyWasEnabled; + #endregion Shadow and UI Culling Settings +} \ No newline at end of file diff --git a/AvatarCloneTest/AvatarClone/AvatarClone.Init.cs b/AvatarCloneTest/AvatarClone/AvatarClone.Init.cs new file mode 100644 index 0000000..41bd84b --- /dev/null +++ b/AvatarCloneTest/AvatarClone/AvatarClone.Init.cs @@ -0,0 +1,128 @@ +using ABI_RC.Core.Player.ShadowClone; +using UnityEngine; + +namespace NAK.AvatarCloneTest; + +public partial class AvatarClone +{ + #region Initialization + + private void InitializeCollections() + { + _standardRenderers = new List(); + _standardFilters = new List(); + _skinnedRenderers = new List(); + _allSourceRenderers = new List(); + + _standardClones = new List(); + _standardCloneFilters = new List(); + _skinnedClones = new List(); + + _standardRenderersNeedingChecks = new List(); + _skinnedRenderersNeedingChecks = new List(); + _cachedSkinnedBoneCounts = new List(); + _cachedSharedMeshes = new List(); + + _localMaterials = new List(); + _cullingMaterials = new List(); + _mainMaterials = new List(); + _propertyBlock = new MaterialPropertyBlock(); + + _blendShapeWeights = new List>(); + } + + private void InitializeRenderers() + { + var renderers = GetComponentsInChildren(true); + + // Pre-size lists based on found renderers + // _standardRenderers.Capacity = renderers.Length; + // _standardFilters.Capacity = renderers.Length; + // _skinnedRenderers.Capacity = renderers.Length; + // _allSourceRenderers.Capacity = renderers.Length; + + // Sort renderers into their respective lists + foreach (Renderer render in renderers) + { + _allSourceRenderers.Add(render); + + switch (render) + { + case MeshRenderer meshRenderer: + { + MeshFilter filter = meshRenderer.GetComponent(); + if (filter != null && filter.sharedMesh != null) + { + _standardRenderers.Add(meshRenderer); + _standardFilters.Add(filter); + } + break; + } + case SkinnedMeshRenderer skinnedRenderer: + { + if (skinnedRenderer.sharedMesh != null) _skinnedRenderers.Add(skinnedRenderer); + break; + } + } + } + } + + private void SetupMaterialsAndBlendShapes() + { + // Cache counts + int standardCount = _standardRenderers.Count; + int skinnedCount = _skinnedRenderers.Count; + var standardRenderers = _standardRenderers; + var skinnedRenderers = _skinnedRenderers; + var localMats = _localMaterials; + var cullingMats = _cullingMaterials; + var blendWeights = _blendShapeWeights; + + // Setup standard renderer materials + for (int i = 0; i < standardCount; i++) + { + MeshRenderer render = standardRenderers[i]; + int matCount = render.sharedMaterials.Length; + + // Local materials array + var localMatArray = new Material[matCount]; + for (int j = 0; j < matCount; j++) localMatArray[j] = render.sharedMaterials[j]; + localMats.Add(localMatArray); + + // Culling materials array + var cullingMatArray = new Material[matCount]; + for (int j = 0; j < matCount; j++) cullingMatArray[j] = ShadowCloneUtils.cullingMaterial; + cullingMats.Add(cullingMatArray); + } + + // Setup skinned renderer materials and blend shapes + for (int i = 0; i < skinnedCount; i++) + { + SkinnedMeshRenderer render = skinnedRenderers[i]; + int matCount = render.sharedMaterials.Length; + + // Local materials array + var localMatArray = new Material[matCount]; + for (int j = 0; j < matCount; j++) localMatArray[j] = render.sharedMaterials[j]; + localMats.Add(localMatArray); + + // Culling materials array + var cullingMatArray = new Material[matCount]; + for (int j = 0; j < matCount; j++) cullingMatArray[j] = ShadowCloneUtils.cullingMaterial; + cullingMats.Add(cullingMatArray); + + // Blend shape weights + int blendShapeCount = render.sharedMesh.blendShapeCount; + var weights = new List(blendShapeCount); + for (int j = 0; j < blendShapeCount; j++) weights.Add(0f); + blendWeights.Add(weights); + } + + // Initialize renderer state arrays + int totalRenderers = _allSourceRenderers.Count; + _originallyHadShadows = new bool[totalRenderers]; + _originallyWasEnabled = new bool[totalRenderers]; + } + + #endregion Initialization +} \ No newline at end of file diff --git a/AvatarCloneTest/AvatarClone/AvatarClone.MagicaSupport.cs b/AvatarCloneTest/AvatarClone/AvatarClone.MagicaSupport.cs new file mode 100644 index 0000000..7ae5855 --- /dev/null +++ b/AvatarCloneTest/AvatarClone/AvatarClone.MagicaSupport.cs @@ -0,0 +1,34 @@ +using MagicaCloth; +using MagicaCloth2; +using UnityEngine; + +namespace NAK.AvatarCloneTest; + +public partial class AvatarClone +{ + #region Magica Cloth Support + + private void SetupMagicaClothSupport() + { + var magicaCloths1 = GetComponentsInChildren(true); + foreach (MagicaRenderDeformer magicaCloth in magicaCloths1) + { + // Get the renderer on the same object + Renderer renderer = magicaCloth.gameObject.GetComponent(); + SetRendererNeedsAdditionalChecks(renderer, true); + } + + var magicaCloths2 = GetComponentsInChildren(true); + foreach (MagicaCloth2.MagicaCloth magicaCloth in magicaCloths2) + { + if (magicaCloth.serializeData.clothType != ClothProcess.ClothType.MeshCloth) + continue; // Only matters for cloth physics + + // Set the affected renderers as requiring extra checks + var renderers = magicaCloth.serializeData.sourceRenderers; + foreach (Renderer renderer in renderers) SetRendererNeedsAdditionalChecks(renderer, true); + } + } + + #endregion Magica Cloth Support +} \ No newline at end of file diff --git a/AvatarCloneTest/AvatarClone/AvatarClone.Update.cs b/AvatarCloneTest/AvatarClone/AvatarClone.Update.cs new file mode 100644 index 0000000..42cef73 --- /dev/null +++ b/AvatarCloneTest/AvatarClone/AvatarClone.Update.cs @@ -0,0 +1,181 @@ +using UnityEngine; + +namespace NAK.AvatarCloneTest; + +public partial class AvatarClone +{ + #region Update Methods + + private void UpdateStandardRenderers() + { + int count = _standardRenderers.Count; + var sourceRenderers = _standardRenderers; + var cloneRenderers = _standardClones; + var localMats = _localMaterials; + + for (int i = 0; i < count; i++) + { + if (!IsRendererValid(sourceRenderers[i])) continue; + CopyMaterialsAndProperties( + sourceRenderers[i], + cloneRenderers[i], + _propertyBlock, + _mainMaterials, + localMats[i]); + } + } + + private void UpdateSkinnedRenderers() + { + int standardCount = _standardRenderers.Count; + int count = _skinnedRenderers.Count; + var sourceRenderers = _skinnedRenderers; + var cloneRenderers = _skinnedClones; + var localMats = _localMaterials; + var blendWeights = _blendShapeWeights; + + for (int i = 0; i < count; i++) + { + SkinnedMeshRenderer source = sourceRenderers[i]; + if (!IsRendererValid(source)) continue; + + SkinnedMeshRenderer clone = cloneRenderers[i]; + CopyMaterialsAndProperties( + source, + clone, + _propertyBlock, + _mainMaterials, + localMats[i + standardCount]); + + CopyBlendShapes(source, clone, blendWeights[i]); + } + } + + private void UpdateStandardRenderersWithChecks() + { + s_CopyMeshes.Begin(); + + var cloneFilters = _standardCloneFilters; + var sourceFilters = _standardFilters; + var cachedMeshes = _cachedSharedMeshes; + var checkIndices = _standardRenderersNeedingChecks; + int checkCount = checkIndices.Count; + + while (cachedMeshes.Count < checkCount) cachedMeshes.Add(null); + + for (int i = 0; i < checkCount; i++) + { + int rendererIndex = checkIndices[i]; + Mesh newMesh = sourceFilters[rendererIndex].sharedMesh; + if (ReferenceEquals(newMesh, cachedMeshes[i])) continue; + cloneFilters[rendererIndex].sharedMesh = newMesh; // expensive & allocates + cachedMeshes[i] = newMesh; + } + + s_CopyMeshes.End(); + } + + private void UpdateSkinnedRenderersWithChecks() + { + s_CopyMeshes.Begin(); + + var sourceRenderers = _skinnedRenderers; + var cloneRenderers = _skinnedClones; + var cachedMeshes = _cachedSharedMeshes; + var cachedBoneCounts = _cachedSkinnedBoneCounts; + var checkIndices = _skinnedRenderersNeedingChecks; + int checkCount = checkIndices.Count; + int meshOffset = _standardRenderersNeedingChecks.Count; + + // Ensure cache lists are properly sized + while (cachedMeshes.Count < meshOffset + checkCount) cachedMeshes.Add(null); + while (cachedBoneCounts.Count < checkCount) cachedBoneCounts.Add(0); + + for (int i = 0; i < checkCount; i++) + { + int rendererIndex = checkIndices[i]; + SkinnedMeshRenderer source = sourceRenderers[rendererIndex]; + SkinnedMeshRenderer clone = cloneRenderers[rendererIndex]; + + // Check mesh changes + Mesh newMesh = source.sharedMesh; // expensive & allocates + if (!ReferenceEquals(newMesh, cachedMeshes[meshOffset + i])) + { + clone.sharedMesh = newMesh; + cachedMeshes[meshOffset + i] = newMesh; + } + + // Check bone changes + var sourceBones = source.bones; + int newBoneCount = sourceBones.Length; + int oldBoneCount = cachedBoneCounts[i]; + if (newBoneCount == oldBoneCount) + continue; + + var cloneBones = clone.bones; // expensive & allocates + if (newBoneCount > oldBoneCount) + { + // Resize array and copy only the new bones (Magica Cloth appends bones when enabling Mesh Cloth) + Array.Resize(ref cloneBones, newBoneCount); + for (int boneIndex = oldBoneCount; boneIndex < newBoneCount; boneIndex++) + cloneBones[boneIndex] = sourceBones[boneIndex]; + clone.bones = cloneBones; + } + else + { + // If shrinking, just set the whole array + clone.bones = sourceBones; + } + + cachedBoneCounts[i] = newBoneCount; + } + + s_CopyMeshes.End(); + } + + private static void CopyMaterialsAndProperties( + Renderer source, Renderer clone, + MaterialPropertyBlock propertyBlock, + List mainMaterials, + Material[] localMaterials) + { + s_CopyMaterials.Begin(); + + source.GetSharedMaterials(mainMaterials); + + int matCount = mainMaterials.Count; + bool hasChanged = false; + for (var i = 0; i < matCount; i++) + { + if (ReferenceEquals(mainMaterials[i], localMaterials[i])) continue; + localMaterials[i] = mainMaterials[i]; + hasChanged = true; + } + if (hasChanged) clone.sharedMaterials = localMaterials; + + source.GetPropertyBlock(propertyBlock); + clone.SetPropertyBlock(propertyBlock); + + s_CopyMaterials.End(); + } + + private static void CopyBlendShapes( + SkinnedMeshRenderer source, + SkinnedMeshRenderer target, + List blendShapeWeights) + { + s_CopyBlendShapes.Begin(); + + int weightCount = blendShapeWeights.Count; + for (var i = 0; i < weightCount; i++) + { + var weight = source.GetBlendShapeWeight(i); + // ReSharper disable once CompareOfFloatsByEqualityOperator + if (weight == blendShapeWeights[i]) continue; // Halves the work + target.SetBlendShapeWeight(i, blendShapeWeights[i] = weight); + } + + s_CopyBlendShapes.End(); + } + #endregion Update Methods +} \ No newline at end of file diff --git a/AvatarCloneTest/AvatarClone/AvatarClone.Util.cs b/AvatarCloneTest/AvatarClone/AvatarClone.Util.cs new file mode 100644 index 0000000..c6fb033 --- /dev/null +++ b/AvatarCloneTest/AvatarClone/AvatarClone.Util.cs @@ -0,0 +1,20 @@ +using ABI_RC.Core; +using ABI_RC.Core.Player; +using UnityEngine; + +namespace NAK.AvatarCloneTest; + +public partial class AvatarClone +{ + private static bool CameraRendersPlayerLocalLayer(Camera cam) + => (cam.cullingMask & (1 << CVRLayers.PlayerLocal)) != 0; + + private static bool CameraRendersPlayerCloneLayer(Camera cam) + => (cam.cullingMask & (1 << CVRLayers.PlayerClone)) != 0; + + private static bool IsUIInternalCamera(Camera cam) + => cam == PlayerSetup.Instance.activeUiCam; + + private static bool IsRendererValid(Renderer renderer) + => renderer && renderer.gameObject.activeInHierarchy; +} \ No newline at end of file diff --git a/AvatarCloneTest/AvatarClone/AvatarClone.cs b/AvatarCloneTest/AvatarClone/AvatarClone.cs new file mode 100644 index 0000000..88c8112 --- /dev/null +++ b/AvatarCloneTest/AvatarClone/AvatarClone.cs @@ -0,0 +1,134 @@ +using UnityEngine; +using UnityEngine.Rendering; + +namespace NAK.AvatarCloneTest; + +public partial class AvatarClone : MonoBehaviour +{ + #region Unity Events + + private void Start() + { + InitializeCollections(); + InitializeRenderers(); + SetupMaterialsAndBlendShapes(); + CreateClones(); + + InitializeExclusions(); + SetupMagicaClothSupport(); + + Camera.onPreCull += MyOnPreRender; + } + + private void OnDestroy() + { + Camera.onPreCull -= MyOnPreRender; + } + + private void LateUpdate() + { + // Update all renderers with basic properties (Materials & BlendShapes) + UpdateStandardRenderers(); + UpdateSkinnedRenderers(); + + // Additional pass for renderers needing extra checks (Shared Mesh & Bone Changes) + UpdateStandardRenderersWithChecks(); + UpdateSkinnedRenderersWithChecks(); + } + + private void MyOnPreRender(Camera cam) + { + s_MyOnPreRender.Begin(); + + bool isOurUiCamera = IsUIInternalCamera(cam); + bool rendersOurPlayerLayer = CameraRendersPlayerLocalLayer(cam); + bool rendersOurCloneLayer = CameraRendersPlayerCloneLayer(cam); + + // Renders both player layers. + // PlayerLocal will now act as a shadow caster, while PlayerClone will act as the actual head-hidden renderer. + bool rendersBothPlayerLayers = rendersOurPlayerLayer && rendersOurCloneLayer; + if (!_shadowsOnlyActive && rendersBothPlayerLayers) + { + s_SetShadowsOnly.Begin(); + + int sourceCount = _allSourceRenderers.Count; + var sourceRenderers = _allSourceRenderers; + for (int i = 0; i < sourceCount; i++) + { + Renderer renderer = sourceRenderers[i]; + if (!IsRendererValid(renderer)) continue; + + bool shouldRender = renderer.shadowCastingMode != ShadowCastingMode.Off; + _originallyWasEnabled[i] = renderer.enabled; + _originallyHadShadows[i] = shouldRender; + renderer.shadowCastingMode = ShadowCastingMode.ShadowsOnly; + if (renderer.forceRenderingOff == shouldRender) renderer.forceRenderingOff = !shouldRender; // TODO: Eval if check is needed + } + _shadowsOnlyActive = true; + + s_SetShadowsOnly.End(); + } + else if (_shadowsOnlyActive && !rendersBothPlayerLayers) + { + s_UndoShadowsOnly.Begin(); + + int sourceCount = _allSourceRenderers.Count; + var sourceRenderers = _allSourceRenderers; + for (int i = 0; i < sourceCount; i++) + { + Renderer renderer = sourceRenderers[i]; + if (!IsRendererValid(renderer)) continue; + + renderer.shadowCastingMode = _originallyHadShadows[i] ? ShadowCastingMode.On : ShadowCastingMode.Off; + if (renderer.forceRenderingOff == _originallyWasEnabled[i]) renderer.forceRenderingOff = !_originallyWasEnabled[i]; // TODO: Eval if check is needed + } + _shadowsOnlyActive = false; + + s_UndoShadowsOnly.End(); + } + + // Handle UI culling material changes + if (isOurUiCamera && !_uiCullingActive && rendersOurCloneLayer) + { + s_SetUiCulling.Begin(); + + int standardCount = _standardRenderers.Count; + var standardClones = _standardClones; + var cullingMaterials = _cullingMaterials; + for (int i = 0; i < standardCount; i++) + standardClones[i].sharedMaterials = cullingMaterials[i]; + + int skinnedCount = _skinnedRenderers.Count; + var skinnedClones = _skinnedClones; + for (int i = 0; i < skinnedCount; i++) + skinnedClones[i].sharedMaterials = cullingMaterials[i + standardCount]; + + _uiCullingActive = true; + + s_SetUiCulling.End(); + } + else if (!isOurUiCamera && _uiCullingActive) + { + s_UndoUiCulling.Begin(); + + int standardCount = _standardRenderers.Count; + var standardClones = _standardClones; + var localMaterials = _localMaterials; + for (int i = 0; i < standardCount; i++) + standardClones[i].sharedMaterials = localMaterials[i]; + + int skinnedCount = _skinnedRenderers.Count; + var skinnedClones = _skinnedClones; + for (int i = 0; i < skinnedCount; i++) + skinnedClones[i].sharedMaterials = localMaterials[i + standardCount]; + + _uiCullingActive = false; + + s_UndoUiCulling.End(); + } + + s_MyOnPreRender.End(); + } + + #endregion Unity Events +} \ No newline at end of file diff --git a/AvatarCloneTest/AvatarClone/FPRExclusion/AvatarCloneExclusion.cs b/AvatarCloneTest/AvatarClone/FPRExclusion/AvatarCloneExclusion.cs new file mode 100644 index 0000000..5681761 --- /dev/null +++ b/AvatarCloneTest/AvatarClone/FPRExclusion/AvatarCloneExclusion.cs @@ -0,0 +1,38 @@ +using ABI.CCK.Components; +using UnityEngine; + +namespace NAK.AvatarCloneTest; + +public class AvatarCloneExclusion : IExclusionBehaviour +{ + private readonly AvatarClone _cloneSystem; + private readonly Transform _target; + private Transform _shrinkBone; + + public bool isImmuneToGlobalState { get; set; } + + public AvatarCloneExclusion(AvatarClone cloneSystem, Transform target) + { + _cloneSystem = cloneSystem; + _target = target; + } + + public void UpdateExclusions(bool isShown, bool shrinkToZero) + { + Debug.Log($"[AvatarClone2] Updating exclusion for {_target.name}: isShown={isShown}, shrinkToZero={shrinkToZero}"); + + if (_shrinkBone == null) + { + // Create shrink bone parented directly to target + _shrinkBone = new GameObject($"{_target.name}_Shrink").transform; + _shrinkBone.SetParent(_target, false); + Debug.Log($"[AvatarClone2] Created shrink bone for {_target.name}"); + } + + // Set scale based on shrink mode + _shrinkBone.localScale = shrinkToZero ? Vector3.zero : Vector3.positiveInfinity; + + // Let the clone system handle the update + _cloneSystem.HandleExclusionUpdate(_target, _shrinkBone, isShown); + } +} \ No newline at end of file diff --git a/AvatarCloneTest/AvatarCloneTest.csproj b/AvatarCloneTest/AvatarCloneTest.csproj new file mode 100644 index 0000000..2e58669 --- /dev/null +++ b/AvatarCloneTest/AvatarCloneTest.csproj @@ -0,0 +1,9 @@ + + + + LocalCloneFix + + + TRACE;TRACE; + + diff --git a/AvatarCloneTest/Main.cs b/AvatarCloneTest/Main.cs new file mode 100644 index 0000000..21a34bf --- /dev/null +++ b/AvatarCloneTest/Main.cs @@ -0,0 +1,72 @@ +using ABI_RC.Core; +using MelonLoader; +using UnityEngine; + +namespace NAK.AvatarCloneTest; + +public class AvatarCloneTestMod : MelonMod +{ + #region Melon Preferences + + private static readonly MelonPreferences_Category Category = + MelonPreferences.CreateCategory(nameof(AvatarCloneTest)); + + internal static readonly MelonPreferences_Entry EntryUseAvatarCloneTest = + Category.CreateEntry("use_avatar_clone_test", true, + "Use Avatar Clone", description: "Uses the Avatar Clone setup for the local avatar."); + + // internal static readonly MelonPreferences_Entry EntryCopyBlendShapes = + // Category.CreateEntry("copy_blend_shapes", true, + // "Copy Blend Shapes", description: "Copies the blend shapes from the original avatar to the clone."); + // + // internal static readonly MelonPreferences_Entry EntryCopyMaterials = + // Category.CreateEntry("copy_materials", true, + // "Copy Materials", description: "Copies the materials from the original avatar to the clone."); + // + // internal static readonly MelonPreferences_Entry EntryCopyMeshes = + // Category.CreateEntry("copy_meshes", true, + // "Copy Meshes", description: "Copies the meshes from the original avatar to the clone."); + + #endregion Melon Preferences + + #region Melon Events + + public override void OnInitializeMelon() + { + ApplyPatches(typeof(Patches)); // slapped together a fix cause HarmonyInstance.Patch was null ref for no reason? + } + + public override void OnUpdate() + { + // press f1 to find all cameras that arent tagged main and set them tno not render CVRLayers.PlayerClone + if (Input.GetKeyDown(KeyCode.F1)) + { + foreach (var camera in UnityEngine.Object.FindObjectsOfType()) + { + if (camera.tag != "MainCamera") + { + camera.cullingMask &= ~(1 << CVRLayers.PlayerClone); + } + } + } + } + + #endregion Melon Events + + #region Melon Mod Utilities + + private void ApplyPatches(Type type) + { + try + { + HarmonyInstance.PatchAll(type); + } + catch (Exception e) + { + LoggerInstance.Msg($"Failed while patching {type.Name}!"); + LoggerInstance.Error(e); + } + } + + #endregion Melon Mod Utilities +} \ No newline at end of file diff --git a/AvatarCloneTest/Patches.cs b/AvatarCloneTest/Patches.cs new file mode 100644 index 0000000..b4cec17 --- /dev/null +++ b/AvatarCloneTest/Patches.cs @@ -0,0 +1,87 @@ +using ABI_RC.Core; +using ABI_RC.Core.Player; +using ABI_RC.Core.Player.TransformHider; +using ABI_RC.Core.Savior; +using ABI_RC.Systems.Camera; +using HarmonyLib; +using UnityEngine; + +namespace NAK.AvatarCloneTest; + +public static class Patches +{ + [HarmonyPrefix] + [HarmonyPatch(typeof(TransformHiderUtils), nameof(TransformHiderUtils.SetupAvatar))] + private static bool OnSetupAvatar(GameObject avatar) + { + if (!AvatarCloneTestMod.EntryUseAvatarCloneTest.Value) return true; + avatar.AddComponent(); + return false; + } + + // [HarmonyPostfix] + // [HarmonyPatch(typeof(FPRExclusion), nameof(FPRExclusion.UpdateExclusions))] + // private static void OnUpdateExclusions(ref FPRExclusion __instance) + // { + // AvatarClone clone = PlayerSetup.Instance._avatar.GetComponent(); + // if (clone == null) return; + // clone.SetBoneChainVisibility(__instance.target, !__instance.isShown, !__instance.shrinkToZero); + // } + + [HarmonyPostfix] + [HarmonyPatch(typeof(CVRMirror), nameof(CVRMirror.Start))] + private static void OnMirrorStart(CVRMirror __instance) + { + if (!AvatarCloneTestMod.EntryUseAvatarCloneTest.Value) + return; + + // Don't reflect the player clone layer + __instance.m_ReflectLayers &= ~(1 << CVRLayers.PlayerClone); + } + + [HarmonyPostfix] + [HarmonyPatch(typeof(PlayerSetup), nameof(PlayerSetup.Update))] + private static void OnTransformHiderManagerUpdate(PlayerSetup __instance) + { + if (!AvatarCloneTestMod.EntryUseAvatarCloneTest.Value) + return; + + if (MetaPort.Instance.settings.GetSettingsBool("ExperimentalAvatarOverrenderUI")) + __instance.activeUiCam.cullingMask |= 1 << CVRLayers.PlayerClone; + else + __instance.activeUiCam.cullingMask &= ~(1 << CVRLayers.PlayerClone); + } + + private static bool _wasDebugInPortableCamera; + + [HarmonyPostfix] + [HarmonyPatch(typeof(PortableCamera), nameof(PortableCamera.Update))] + private static void OnPortableCameraUpdate(ref PortableCamera __instance) + { + if (!AvatarCloneTestMod.EntryUseAvatarCloneTest.Value) + { + // Show both PlayerLocal and PlayerClone + __instance.cameraComponent.cullingMask |= 1 << CVRLayers.PlayerLocal; + __instance.cameraComponent.cullingMask |= 1 << CVRLayers.PlayerClone; + return; + } + + if (TransformHiderManager.s_DebugInPortableCamera == _wasDebugInPortableCamera) + return; + + if (TransformHiderManager.s_DebugInPortableCamera) + { + // Hide PlayerLocal, show PlayerClone + __instance.cameraComponent.cullingMask &= ~(1 << CVRLayers.PlayerLocal); + __instance.cameraComponent.cullingMask |= 1 << CVRLayers.PlayerClone; + } + else + { + // Show PlayerLocal, hide PlayerClone + __instance.cameraComponent.cullingMask |= 1 << CVRLayers.PlayerLocal; + __instance.cameraComponent.cullingMask &= ~(1 << CVRLayers.PlayerClone); + } + + _wasDebugInPortableCamera = TransformHiderManager.s_DebugInPortableCamera; + } +} \ No newline at end of file diff --git a/AvatarCloneTest/Properties/AssemblyInfo.cs b/AvatarCloneTest/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..3334b28 --- /dev/null +++ b/AvatarCloneTest/Properties/AssemblyInfo.cs @@ -0,0 +1,32 @@ +using MelonLoader; +using NAK.AvatarCloneTest.Properties; +using System.Reflection; + +[assembly: AssemblyVersion(AssemblyInfoParams.Version)] +[assembly: AssemblyFileVersion(AssemblyInfoParams.Version)] +[assembly: AssemblyInformationalVersion(AssemblyInfoParams.Version)] +[assembly: AssemblyTitle(nameof(NAK.AvatarCloneTest))] +[assembly: AssemblyCompany(AssemblyInfoParams.Author)] +[assembly: AssemblyProduct(nameof(NAK.AvatarCloneTest))] + +[assembly: MelonInfo( + typeof(NAK.AvatarCloneTest.AvatarCloneTestMod), + nameof(NAK.AvatarCloneTest), + AssemblyInfoParams.Version, + AssemblyInfoParams.Author, + downloadLink: "https://github.com/NotAKidoS/NAK_CVR_Mods/tree/main/AvatarCloneTest" +)] + +[assembly: MelonGame("Alpha Blend Interactive", "ChilloutVR")] +[assembly: MelonPlatform(MelonPlatformAttribute.CompatiblePlatforms.WINDOWS_X64)] +[assembly: MelonPlatformDomain(MelonPlatformDomainAttribute.CompatibleDomains.MONO)] +[assembly: MelonColor(255, 246, 25, 99)] // red-pink +[assembly: MelonAuthorColor(255, 158, 21, 32)] // red +[assembly: HarmonyDontPatchAll] + +namespace NAK.AvatarCloneTest.Properties; +internal static class AssemblyInfoParams +{ + public const string Version = "1.0.0"; + public const string Author = "NotAKidoS"; +} \ No newline at end of file diff --git a/AvatarCloneTest/README.md b/AvatarCloneTest/README.md new file mode 100644 index 0000000..cc12a9c --- /dev/null +++ b/AvatarCloneTest/README.md @@ -0,0 +1,18 @@ +# VisualCloneFix + +Fixes the Visual Clone system and allows you to use it again. + +Using the Visual Clone should be faster than the default Head Hiding & Shadow Clones, but will add a longer hitch on initial avatar load. + +**NOTE:** The Visual Clone is still an experimental feature that was temporarily removed in [ChilloutVR 2024r175 Hotfix 1](https://abinteractive.net/blog/chilloutvr_2024r175_hotfix_1), so there may be bugs or issues with it. + +--- + +Here is the block of text where I tell you this mod is not affiliated with or endorsed by ABI. +https://documentation.abinteractive.net/official/legal/tos/#7-modding-our-games + +> This mod is an independent creation not affiliated with, supported by, or approved by Alpha Blend Interactive. + +> Use of this mod is done so at the user's own risk and the creator cannot be held responsible for any issues arising from its use. + +> To the best of my knowledge, I have adhered to the Modding Guidelines established by Alpha Blend Interactive. diff --git a/AvatarCloneTest/format.json b/AvatarCloneTest/format.json new file mode 100644 index 0000000..6634e9f --- /dev/null +++ b/AvatarCloneTest/format.json @@ -0,0 +1,23 @@ +{ + "_id": 221, + "name": "VisualCloneFix", + "modversion": "1.0.1", + "gameversion": "2024r175", + "loaderversion": "0.6.1", + "modtype": "Mod", + "author": "NotAKidoS", + "description": "Fixes the Visual Clone system and allows you to use it again.\n\nUsing the Visual Clone should be faster than the default Head Hiding & Shadow Clones, but will add a longer hitch on initial avatar load.\n\n**NOTE:** The Visual Clone is still an experimental feature that was temporarily removed in [ChilloutVR 2024r175 Hotfix 1](https://abinteractive.net/blog/chilloutvr_2024r175_hotfix_1), so there may be bugs or issues with it.", + "searchtags": [ + "visual", + "clone", + "head", + "hiding" + ], + "requirements": [ + "None" + ], + "downloadlink": "https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r36/VisualCloneFix.dll", + "sourcelink": "https://github.com/NotAKidoS/NAK_CVR_Mods/tree/main/VisualCloneFix/", + "changelog": "- Fixed FPRExclusions IsShown state being inverted when toggled.\n- Fixed head FPRExclusion generation not checking for existing exclusion.\n- Sped up FindExclusionVertList by 100x by not being an idiot. This heavily reduces avatar hitch with Visual Clone active.", + "embedcolor": "#f61963" +} \ No newline at end of file