AvatarCloneTest: push so i can reference in an email

This commit is contained in:
NotAKidoS 2025-01-29 16:37:15 -06:00
parent 6d30fe1f41
commit 3660b8f683
16 changed files with 1155 additions and 0 deletions

View file

@ -0,0 +1,68 @@
using UnityEngine;
namespace NAK.AvatarCloneTest;
public partial class AvatarClone
{
#region Public API Methods
/// <summary>
/// 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.
/// </summary>
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
}

View file

@ -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<MeshRenderer>(standardCount);
_standardCloneFilters = new List<MeshFilter>(standardCount);
for (int i = 0; i < standardCount; i++) CreateStandardClone(i);
int skinnedCount = _skinnedRenderers.Count;
_skinnedClones = new List<SkinnedMeshRenderer>(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<MeshRenderer>();
MeshFilter cloneFilter = go.AddComponent<MeshFilter>();
// 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<SkinnedMeshRenderer>();
// 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
}

View file

@ -0,0 +1,141 @@
using ABI.CCK.Components;
using UnityEngine;
namespace NAK.AvatarCloneTest;
public partial class AvatarClone
{
private readonly Dictionary<Transform, HashSet<Renderer>> _exclusionDirectRenderers = new();
private readonly Dictionary<Transform, HashSet<Transform>> _exclusionControlledBones = new();
private void InitializeExclusions()
{
// Add head exclusion for humanoid avatars if not present
var animator = GetComponent<Animator>();
if (animator != null && animator.isHuman)
{
var headBone = animator.GetBoneTransform(HumanBodyBones.Head);
if (headBone != null && headBone.GetComponent<FPRExclusion>() == null)
{
var exclusion = headBone.gameObject.AddComponent<FPRExclusion>();
exclusion.isShown = false;
exclusion.target = headBone;
exclusion.shrinkToZero = true;
}
}
// Process existing exclusions bottom-up
var exclusions = GetComponentsInChildren<FPRExclusion>(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<Renderer>();
_exclusionControlledBones[exclusion.target] = new HashSet<Transform>();
// 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<Transform>();
stack.Push(target);
while (stack.Count > 0)
{
var current = stack.Pop();
// Skip if this transform belongs to another exclusion
if (current != target && current.GetComponent<FPRExclusion>() != null)
continue;
_exclusionControlledBones[target].Add(current);
// Add renderers that will need their clone visibility toggled
foreach (var renderer in current.GetComponents<Renderer>())
{
// 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;
}
}
}
}

View file

@ -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<MeshRenderer> _standardRenderers;
private List<MeshFilter> _standardFilters;
private List<SkinnedMeshRenderer> _skinnedRenderers;
private List<Renderer> _allSourceRenderers; // For shadow casting only
#endregion Source Renderers
#region Clone Renderers
private List<MeshRenderer> _standardClones;
private List<MeshFilter> _standardCloneFilters;
private List<SkinnedMeshRenderer> _skinnedClones;
#endregion Clone Renderers
#region Dynamic Check Lists
private List<int> _standardRenderersNeedingChecks; // Stores indices into _standardRenderers
private List<int> _skinnedRenderersNeedingChecks; // Stores indices into _skinnedRenderers
private List<int> _cachedSkinnedBoneCounts; // So we don't copy the bones unless they've changed
private List<Mesh> _cachedSharedMeshes; // So we don't copy the mesh unless it's changed
#endregion Dynamic Check Lists
#region Material Data
private List<Material[]> _localMaterials;
private List<Material[]> _cullingMaterials;
private List<Material> _mainMaterials;
private MaterialPropertyBlock _propertyBlock;
#endregion Material Data
#region Blend Shape Data
private List<List<float>> _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
}

View file

@ -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<MeshRenderer>();
_standardFilters = new List<MeshFilter>();
_skinnedRenderers = new List<SkinnedMeshRenderer>();
_allSourceRenderers = new List<Renderer>();
_standardClones = new List<MeshRenderer>();
_standardCloneFilters = new List<MeshFilter>();
_skinnedClones = new List<SkinnedMeshRenderer>();
_standardRenderersNeedingChecks = new List<int>();
_skinnedRenderersNeedingChecks = new List<int>();
_cachedSkinnedBoneCounts = new List<int>();
_cachedSharedMeshes = new List<Mesh>();
_localMaterials = new List<Material[]>();
_cullingMaterials = new List<Material[]>();
_mainMaterials = new List<Material>();
_propertyBlock = new MaterialPropertyBlock();
_blendShapeWeights = new List<List<float>>();
}
private void InitializeRenderers()
{
var renderers = GetComponentsInChildren<Renderer>(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<MeshFilter>();
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<float>(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
}

View file

@ -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<MagicaRenderDeformer>(true);
foreach (MagicaRenderDeformer magicaCloth in magicaCloths1)
{
// Get the renderer on the same object
Renderer renderer = magicaCloth.gameObject.GetComponent<Renderer>();
SetRendererNeedsAdditionalChecks(renderer, true);
}
var magicaCloths2 = GetComponentsInChildren<MagicaCloth2.MagicaCloth>(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
}

View file

@ -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<Material> 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<float> 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
}

View file

@ -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;
}

View file

@ -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
}

View file

@ -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);
}
}

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RootNamespace>LocalCloneFix</RootNamespace>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DefineConstants>TRACE;TRACE;</DefineConstants>
</PropertyGroup>
</Project>

72
AvatarCloneTest/Main.cs Normal file
View file

@ -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<bool> 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<bool> 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<bool> EntryCopyMaterials =
// Category.CreateEntry("copy_materials", true,
// "Copy Materials", description: "Copies the materials from the original avatar to the clone.");
//
// internal static readonly MelonPreferences_Entry<bool> 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<UnityEngine.Camera>())
{
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
}

View file

@ -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<AvatarClone>();
return false;
}
// [HarmonyPostfix]
// [HarmonyPatch(typeof(FPRExclusion), nameof(FPRExclusion.UpdateExclusions))]
// private static void OnUpdateExclusions(ref FPRExclusion __instance)
// {
// AvatarClone clone = PlayerSetup.Instance._avatar.GetComponent<AvatarClone>();
// 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;
}
}

View file

@ -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";
}

18
AvatarCloneTest/README.md Normal file
View file

@ -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.

View file

@ -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"
}