[BetterShadowClone] Throwing on git before i completely overcomplicate and have to revert lmao

This commit is contained in:
NotAKidoS 2024-02-03 02:12:40 -06:00
parent d155ea546e
commit 5ee7dca50b
18 changed files with 1697 additions and 0 deletions

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<EmbeddedResource Include="Resources\bettershadowclone.assets">
<LogicalName>bettershadowclone.assets</LogicalName>
</EmbeddedResource>
</ItemGroup>
</Project>

View file

@ -0,0 +1,130 @@
// using System.Collections.Generic;
// using System.Linq;
// using NAK.BetterShadowClone;
// using UnityEngine;
// using UnityEngine.Serialization;
//
// namespace Koneko.BetterHeadHider;
//
// public class BoneHider : MonoBehaviour {
// public static ComputeShader shader;
//
// public SkinnedMeshRenderer[] targets;
// public Transform shrinkBone;
// private ComputeBuffer[] weightedBuffers;
// private int[] weightedCounts;
// private int[] bufferLayouts;
// private int[] threadGroups;
// private bool Initialized;
//
//
// #region Public Methods
// public static void SetupAvatar(GameObject avatar, Transform shrinkBone, SkinnedMeshRenderer[] targets) {
// BoneHider shrink = avatar.AddComponent<BoneHider>();
// shrink.shrinkBone = shrinkBone;
// shrink.targets = targets;
// }
// #endregion
//
// #region Private Methods
// private void Initialize() {
// Dispose();
//
// weightedBuffers = new ComputeBuffer[targets.Length];
// weightedCounts = new int[targets.Length];
// bufferLayouts = new int[targets.Length];
// threadGroups = new int[targets.Length];
//
// for (int i = 0; i < targets.Length; i++) {
// List<int> weighted = FindHeadVertices(targets[i]);
// if (weighted.Count == 0) continue;
//
// targets[i].sharedMesh.vertexBufferTarget |= GraphicsBuffer.Target.Raw;
// weightedBuffers[i] = new(weighted.Count, sizeof(int));
// weightedBuffers[i].SetData(weighted.ToArray());
// weightedCounts[i] = weighted.Count;
//
// int bufferLayout = 0;
// if (targets[i].sharedMesh.HasVertexAttribute(UnityEngine.Rendering.VertexAttribute.Position)) bufferLayout += 3;
// if (targets[i].sharedMesh.HasVertexAttribute(UnityEngine.Rendering.VertexAttribute.Normal)) bufferLayout += 3;
// if (targets[i].sharedMesh.HasVertexAttribute(UnityEngine.Rendering.VertexAttribute.Tangent)) bufferLayout += 4;
// bufferLayouts[i] = bufferLayout;
//
// threadGroups[i] = Mathf.CeilToInt(weighted.Count / 64.0f);
// Debug.Log(threadGroups[i]);
// }
//
// Initialized = true;
// }
//
// private List<int> FindHeadVertices(SkinnedMeshRenderer target) {
// List<int> headVertices = new();
// BoneWeight[] boneWeights = target.sharedMesh.boneWeights;
// HashSet<Transform> bones = new();
//
// bones = shrinkBone.GetComponentsInChildren<Transform>(true).ToHashSet();
//
// //get indexs of child bones
// HashSet<int> weights = new();
// for (int i = 0; i < target.bones.Length; i++) {
// if (bones.Contains(target.bones[i])) weights.Add(i);
// }
//
// for (int i = 0; i < boneWeights.Length; i++) {
// BoneWeight weight = boneWeights[i];
// if (weights.Contains(weight.boneIndex0) || weights.Contains(weight.boneIndex1) ||
// weights.Contains(weight.boneIndex2) || weights.Contains(weight.boneIndex3)) {
// headVertices.Add(i);
// }
// }
// return headVertices;
// }
//
// private void MyOnPreRender(Camera cam) {
// if (!Initialized)
// return;
//
// // NOTE: We only hide head once, so any camera rendered after wont see the head!
//
// if (cam != ShadowCloneMod.PlayerCamera // only hide in player cam, or in portable cam if debug is on
// && (!ModSettings.EntryHideInPortableCamera.Value || cam != ShadowCloneMod.HandCamera))
// return;
//
// if (!ShadowCloneMod.CheckWantsToHideHead(cam))
// return; // listener said no (Third Person, etc)
//
// for (int i = 0; i < targets.Length; i++) {
// SkinnedMeshRenderer target = targets[i];
//
// if (target == null
// || !target.gameObject.activeInHierarchy
// || weightedBuffers[i] == null) continue;
//
// GraphicsBuffer vertexBuffer = targets[i].GetVertexBuffer();
// if(vertexBuffer == null) continue;
//
// shader.SetVector(s_Pos, Vector3.positiveInfinity); // todo: fix
// shader.SetInt(s_WeightedCount, weightedCounts[i]);
// shader.SetInt(s_BufferLayout, bufferLayouts[i]);
// shader.SetBuffer(0, s_WeightedVertices, weightedBuffers[i]);
// shader.SetBuffer(0, s_VertexBuffer, vertexBuffer);
// shader.Dispatch(0, threadGroups[i], 1, 1);
// vertexBuffer.Dispose();
// }
// }
//
// private void Dispose() {
// Initialized = false;
// if (weightedBuffers == null) return;
// foreach (ComputeBuffer c in weightedBuffers)
// c?.Dispose();
// }
// #endregion
//
// #region Unity Events
// private void OnEnable() => Camera.onPreRender += MyOnPreRender;
// private void OnDisable() => Camera.onPreRender -= MyOnPreRender;
// private void OnDestroy() => Dispose();
// private void Start() => Initialize();
// #endregion
// }

144
BetterShadowClone/Main.cs Normal file
View file

@ -0,0 +1,144 @@
using System;
using System.IO;
using MelonLoader;
using System.Reflection;
using ABI_RC.Core.Player;
using ABI_RC.Core.Util;
using ABI_RC.Core.Util.AssetFiltering;
using ABI_RC.Systems.Camera;
using UnityEngine;
namespace NAK.BetterShadowClone;
public class ShadowCloneMod : MelonMod
{
internal static MelonLogger.Instance Logger;
public override void OnInitializeMelon()
{
Logger = LoggerInstance;
ModSettings.Initialize();
//VRModeSwitchEvents.OnCompletedVRModeSwitch.AddListener(_ => FindCameras());
SharedFilter._avatarWhitelist.Add(typeof(FPRExclusion));
SharedFilter._localComponentWhitelist.Add(typeof(FPRExclusion));
try
{
LoadAssetBundle();
InitializePatches();
}
catch (Exception e)
{
Logger.Error(e);
}
}
#region Hide Head Override
/// <summary>
/// Return false to prevent the head from being hidden.
/// </summary>
public static WantsToHideHeadDelegate wantsToHideHead;
public delegate bool WantsToHideHeadDelegate(Camera cam);
public static bool CheckWantsToHideHead(Camera cam)
{
if (wantsToHideHead == null)
return true;
foreach (Delegate @delegate in wantsToHideHead.GetInvocationList())
{
WantsToHideHeadDelegate method = (WantsToHideHeadDelegate)@delegate;
if (!method(cam)) return false;
}
return true;
}
#endregion
#region Asset Bundle Loading
private const string BetterShadowCloneAssets = "bettershadowclone.assets";
private const string BoneHiderComputePath = "Assets/Koneko/ComputeShaders/BoneHider.compute";
//private const string MeshCopyComputePath = "Assets/Koneko/ComputeShaders/MeshCopy.compute";
private const string ShadowCloneComputePath = "Assets/NotAKid/Shaders/ShadowClone.compute";
private const string ShadowCloneShaderPath = "Assets/NotAKid/Shaders/ShadowClone.shader";
private const string DummyCloneShaderPath = "Assets/NotAKid/Shaders/DummyClone.shader";
private void LoadAssetBundle()
{
Logger.Msg($"Loading required asset bundle...");
using Stream resourceStream = MelonAssembly.Assembly.GetManifestResourceStream(BetterShadowCloneAssets);
using MemoryStream memoryStream = new();
if (resourceStream == null) {
Logger.Error($"Failed to load {BetterShadowCloneAssets}!");
return;
}
resourceStream.CopyTo(memoryStream);
AssetBundle assetBundle = AssetBundle.LoadFromMemory(memoryStream.ToArray());
if (assetBundle == null) {
Logger.Error($"Failed to load {BetterShadowCloneAssets}! Asset bundle is null!");
return;
}
// load shaders
ComputeShader shader = assetBundle.LoadAsset<ComputeShader>(BoneHiderComputePath);
shader.hideFlags |= HideFlags.DontUnloadUnusedAsset;
TransformHiderManager.shader = shader;
Logger.Msg($"Loaded {BoneHiderComputePath}!");
// load shadow clone shader
ComputeShader shadowCloneCompute = assetBundle.LoadAsset<ComputeShader>(ShadowCloneComputePath);
shadowCloneCompute.hideFlags |= HideFlags.DontUnloadUnusedAsset;
ShadowCloneHelper.shader = shadowCloneCompute;
Logger.Msg($"Loaded {ShadowCloneComputePath}!");
// load shadow clone material
Shader shadowCloneShader = assetBundle.LoadAsset<Shader>(ShadowCloneShaderPath);
shadowCloneShader.hideFlags |= HideFlags.DontUnloadUnusedAsset;
ShadowCloneHelper.shadowMaterial = new Material(shadowCloneShader);
Logger.Msg($"Loaded {ShadowCloneShaderPath}!");
Logger.Msg("Asset bundle successfully loaded!");
}
#endregion
#region Harmony Patches
private void InitializePatches()
{
HarmonyInstance.Patch(
typeof(TransformHiderForMainCamera).GetMethod(nameof(TransformHiderForMainCamera.ProcessHierarchy)),
prefix: new HarmonyLib.HarmonyMethod(typeof(ShadowCloneMod).GetMethod(nameof(OnTransformHiderForMainCamera_ProcessHierarchy_Prefix), BindingFlags.NonPublic | BindingFlags.Static))
);
HarmonyInstance.Patch(
typeof(PlayerSetup).GetMethod(nameof(PlayerSetup.ClearAvatar)),
prefix: new HarmonyLib.HarmonyMethod(typeof(ShadowCloneMod).GetMethod(nameof(OnPlayerSetup_ClearAvatar_Prefix), BindingFlags.NonPublic | BindingFlags.Static))
);
}
private static void OnPlayerSetup_ClearAvatar_Prefix()
{
TransformHiderManager.Instance.OnAvatarCleared();
ShadowCloneManager.Instance.OnAvatarCleared();
}
private static void OnTransformHiderForMainCamera_ProcessHierarchy_Prefix(ref bool __runOriginal)
{
if (!__runOriginal || (__runOriginal = !ModSettings.EntryEnabled.Value))
return; // if something else disabled, or we are disabled, don't run
ShadowCloneHelper.SetupAvatar(PlayerSetup.Instance._avatar);
}
#endregion
}

View file

@ -0,0 +1,45 @@
using MelonLoader;
using UnityEngine;
namespace NAK.BetterShadowClone;
public static class ModSettings
{
#region Melon Prefs
private const string SettingsCategory = nameof(ShadowCloneMod);
private static readonly MelonPreferences_Category Category =
MelonPreferences.CreateCategory(SettingsCategory);
internal static readonly MelonPreferences_Entry<bool> EntryEnabled =
Category.CreateEntry("Enabled", true,
description: "Enable Mirror Clone.");
internal static readonly MelonPreferences_Entry<bool> EntryUseShadowClone =
Category.CreateEntry("Use Shadow Clone", true,
description: "Should you have shadow clones?");
internal static readonly MelonPreferences_Entry<bool> EntryCopyMaterialToShadow =
Category.CreateEntry("Copy Material to Shadow", true,
description: "Should the shadow clone copy the material from the original mesh? Note: This can have a slight performance hit.");
internal static readonly MelonPreferences_Entry<bool> EntryDebugHeadHide =
Category.CreateEntry("Debug Head Hide", false,
description: "Should head be hidden for first render?");
#endregion
internal static void Initialize()
{
foreach (MelonPreferences_Entry setting in Category.Entries)
setting.OnEntryValueChangedUntyped.Subscribe(OnSettingsChanged);
}
private static void OnSettingsChanged(object oldValue = null, object newValue = null)
{
TransformHiderManager.s_DebugHeadHide = EntryDebugHeadHide.Value;
ShadowCloneManager.s_CopyMaterialsToShadow = EntryCopyMaterialToShadow.Value;
}
}

View file

@ -0,0 +1,29 @@
using MelonLoader;
using NAK.BetterShadowClone.Properties;
using System.Reflection;
[assembly: AssemblyVersion(AssemblyInfoParams.Version)]
[assembly: AssemblyFileVersion(AssemblyInfoParams.Version)]
[assembly: AssemblyInformationalVersion(AssemblyInfoParams.Version)]
[assembly: AssemblyTitle(nameof(NAK.BetterShadowClone))]
[assembly: AssemblyCompany(AssemblyInfoParams.Author)]
[assembly: AssemblyProduct(nameof(NAK.BetterShadowClone))]
[assembly: MelonInfo(
typeof(NAK.BetterShadowClone.ShadowCloneMod),
nameof(NAK.BetterShadowClone),
AssemblyInfoParams.Version,
AssemblyInfoParams.Author,
downloadLink: "https://github.com/NotAKidOnSteam/NAK_CVR_Mods/tree/main/ShadowCloneMod"
)]
[assembly: MelonGame("Alpha Blend Interactive", "ChilloutVR")]
[assembly: MelonPlatform(MelonPlatformAttribute.CompatiblePlatforms.WINDOWS_X64)]
[assembly: MelonPlatformDomain(MelonPlatformDomainAttribute.CompatibleDomains.MONO)]
namespace NAK.BetterShadowClone.Properties;
internal static class AssemblyInfoParams
{
public const string Version = "1.0.0";
public const string Author = "NotAKidoS & Exterrata";
}

View file

@ -0,0 +1,16 @@
# EzCurls
A mod that allows you to tune your finger curls to your liking. Supposedly can help with VR sign language, as raw finger curls are not that great for quick and precise gestures.
The settings are not too coherent, it is mostly a bunch of things thrown at the wall, but it works for me. I hope it works for you too.
---
Here is the block of text where I tell you this mod is not affiliated or endorsed by ABI.
https://documentation.abinteractive.net/official/legal/tos/#7-modding-our-games
> This mod is an independent creation and is 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.

Binary file not shown.

View file

@ -0,0 +1,11 @@
using System;
namespace NAK.BetterShadowClone;
public interface IShadowClone : IDisposable
{
bool IsValid { get; }
bool Process();
void RenderForShadow();
void RenderForUiCulling();
}

View file

@ -0,0 +1,154 @@
using System;
using UnityEngine;
using UnityEngine.Rendering;
namespace NAK.BetterShadowClone;
public struct MeshShadowClone : IShadowClone
{
// We technically don't need a clone mesh for MeshRenderer shadow clone handling,
// but as the shadows are also utilized for UI culling, we need to have a clone mesh.
// If we don't stick with UI culling, we can just set the shadowCastingMode to ShadowsOnly when player camera renders.
// lame 2 frame init stuff
private const int FrameInitCount = 0;
private int _frameInitCounter;
private bool _hasInitialized;
// shadow is used to cull ui, clone always exists
private readonly bool _shouldCastShadows;
private readonly MeshRenderer _mainMesh;
private readonly MeshRenderer _shadowMesh;
private readonly MeshFilter _shadowMeshFilter;
// material copying (unity is shit)
private bool _hasShadowMaterials;
private readonly Material[] _shadowMaterials;
private readonly MaterialPropertyBlock _shadowMaterialBlock;
#region IShadowClone Methods
public bool IsValid => _mainMesh != null && _shadowMesh != null;
public MeshShadowClone(MeshRenderer meshRenderer)
{
_mainMesh = meshRenderer;
MeshFilter _mainMeshFilter = meshRenderer.GetComponent<MeshFilter>();
if (_mainMesh == null
|| _mainMesh.sharedMaterials == null
|| _mainMesh.sharedMaterials.Length == 0
|| _mainMeshFilter == null
|| _mainMeshFilter.sharedMesh == null)
{
Dispose();
return; // no mesh!
}
_shouldCastShadows = _mainMesh.shadowCastingMode != ShadowCastingMode.Off;
_mainMesh.shadowCastingMode = ShadowCastingMode.Off; // visual mesh doesn't cast shadows
(_shadowMesh, _shadowMeshFilter) = ShadowCloneManager.InstantiateShadowClone(_mainMesh);
_shadowMesh.forceRenderingOff = true;
// material copying shit
int materialCount = _mainMesh.sharedMaterials.Length;
Material shadowMaterial = ShadowCloneHelper.shadowMaterial;
_shadowMaterialBlock = new MaterialPropertyBlock();
_shadowMaterials = new Material[materialCount];
for (int i = 0; i < materialCount; i++) _shadowMaterials[i] = shadowMaterial;
}
public bool Process()
{
bool shouldRender = _mainMesh.enabled && _mainMesh.gameObject.activeInHierarchy;
// copying behaviour of SkinnedShadowClone, to visually be the same when a mesh toggles
if (!shouldRender)
{
_frameInitCounter = 0;
_hasInitialized = false;
_shadowMesh.forceRenderingOff = true;
return false;
}
if (_frameInitCounter >= FrameInitCount)
{
if (_hasInitialized)
return true;
_hasInitialized = true;
return true;
}
_frameInitCounter++;
return false;
}
public void RenderForShadow()
{
_shadowMesh.shadowCastingMode = ShadowCastingMode.ShadowsOnly;
_shadowMesh.forceRenderingOff = !_shouldCastShadows;
// shadow casting needs clone to have original materials (uv discard)
// we also want to respect material swaps... but this is fucking slow :(
if (!ShadowCloneManager.s_CopyMaterialsToShadow)
return;
if (_hasShadowMaterials)
{
// NOTE: will not handle material swaps unless Avatar Overrender Ui is on
_shadowMesh.sharedMaterials = _mainMesh.sharedMaterials;
_hasShadowMaterials = false;
}
UpdateCloneMaterialProperties();
}
public void RenderForUiCulling()
{
_shadowMesh.shadowCastingMode = ShadowCastingMode.On;
_shadowMesh.forceRenderingOff = false;
// UI culling needs clone to have write-to-depth shader
if (_hasShadowMaterials) return;
_shadowMesh.sharedMaterials = _shadowMaterials;
_hasShadowMaterials = true;
// Not needed- MaterialPropertyBlock applied to renderer in RenderForShadow
//UpdateCloneMaterialProperties();
}
public void Dispose()
{
if (_shadowMesh == null)
return; // uh oh
// Cleanup instanced Mesh & Materials
GameObject shadowMeshObject = _shadowMesh.gameObject;
UnityEngine.Object.Destroy(_shadowMeshFilter.sharedMesh);
UnityEngine.Object.Destroy(_shadowMeshFilter);
if (!_hasShadowMaterials)
{
var materials = _shadowMesh.sharedMaterials;
foreach (Material mat in materials) UnityEngine.Object.Destroy(mat);
}
UnityEngine.Object.Destroy(_shadowMesh);
UnityEngine.Object.Destroy(shadowMeshObject);
}
#endregion
#region Private Methods
private void UpdateCloneMaterialProperties()
{
// copy material properties to shadow clone materials
_mainMesh.GetPropertyBlock(_shadowMaterialBlock);
_shadowMesh.SetPropertyBlock(_shadowMaterialBlock);
}
#endregion
}

View file

@ -0,0 +1,223 @@
using System;
using UnityEngine;
using UnityEngine.Rendering;
namespace NAK.BetterShadowClone;
public class SkinnedShadowClone : IShadowClone
{
private static readonly int s_SourceBufferId = Shader.PropertyToID("_sourceBuffer");
private static readonly int s_TargetBufferId = Shader.PropertyToID("_targetBuffer");
private static readonly int s_SourceBufferLayoutId = Shader.PropertyToID("_sourceBufferLayout");
private static readonly int s_SourceRootMatrix = Shader.PropertyToID("_rootBoneMatrix");
// lame 2 frame init stuff
private const int FrameInitCount = 0;
private int _frameInitCounter;
private bool _hasInitialized;
// shadow is used to cull ui, clone always exists
private readonly bool _shouldCastShadows;
private readonly SkinnedMeshRenderer _mainMesh;
private readonly MeshRenderer _shadowMesh;
private readonly MeshFilter _shadowMeshFilter;
private readonly Transform _rootBone;
// clone copying
private GraphicsBuffer _graphicsBuffer;
private GraphicsBuffer _targetBuffer;
private int _threadGroups;
private int _bufferLayout;
// material copying (unity is shit)
private bool _hasShadowMaterials;
private readonly Material[] _shadowMaterials;
private readonly MaterialPropertyBlock _shadowMaterialBlock;
#region IShadowClone Methods
// anything player can touch is suspect to death
public bool IsValid => _mainMesh != null && _shadowMesh != null && _rootBone != null;
internal SkinnedShadowClone(SkinnedMeshRenderer renderer)
{
_mainMesh = renderer;
if (_mainMesh == null
|| _mainMesh.sharedMesh == null
|| _mainMesh.sharedMaterials == null
|| _mainMesh.sharedMaterials.Length == 0)
{
Dispose();
return; // no mesh!
}
_shouldCastShadows = _mainMesh.shadowCastingMode != ShadowCastingMode.Off;
_mainMesh.shadowCastingMode = ShadowCastingMode.Off; // visual mesh doesn't cast shadows
(_shadowMesh, _shadowMeshFilter) = ShadowCloneManager.InstantiateShadowClone(_mainMesh);
_shadowMesh.forceRenderingOff = true;
_rootBone = _mainMesh.rootBone;
_rootBone ??= _mainMesh.transform; // fallback to transform if no root bone
// material copying shit
int materialCount = _mainMesh.sharedMaterials.Length;
Material shadowMaterial = ShadowCloneHelper.shadowMaterial;
_shadowMaterialBlock = new MaterialPropertyBlock(); // TODO: check if we need one per material on renderer, idk if this is only first index
_shadowMaterials = new Material[materialCount];
for (int i = 0; i < materialCount; i++) _shadowMaterials[i] = shadowMaterial;
}
public bool Process()
{
// some people animate renderer.enabled instead of gameObject.activeInHierarchy
// do not disable shadow clone game object, it causes a flicker when re-enabled!
bool shouldRender = _mainMesh.enabled && _mainMesh.gameObject.activeInHierarchy;
// GraphicsBuffer becomes stale when mesh is disabled
if (!shouldRender)
{
_frameInitCounter = 0;
_hasInitialized = false;
_shadowMesh.forceRenderingOff = true; // force off if mesh is disabled
return false; // TODO: dispose stale buffers
}
// Unity is weird, so we need to wait 2 frames before we can get the graphics buffer
if (_frameInitCounter >= FrameInitCount)
{
if (_hasInitialized)
return true;
_hasInitialized = true;
SetupGraphicsBuffer();
return true;
}
_frameInitCounter++;
return false;
}
public void RenderForShadow()
{
ResetShadowClone();
RenderShadowClone();
}
public void RenderForUiCulling()
{
ConfigureShadowCloneForUiCulling();
RenderShadowClone();
}
public void Dispose()
{
if (_shadowMesh != null)
{
// Cleanup instanced Mesh & Materials
GameObject shadowMeshObject = _shadowMesh.gameObject;
UnityEngine.Object.Destroy(_shadowMeshFilter.sharedMesh);
UnityEngine.Object.Destroy(_shadowMeshFilter);
if (!_hasShadowMaterials)
{
var materials = _shadowMesh.sharedMaterials;
foreach (Material mat in materials) UnityEngine.Object.Destroy(mat);
}
UnityEngine.Object.Destroy(_shadowMesh);
UnityEngine.Object.Destroy(shadowMeshObject);
}
_graphicsBuffer?.Dispose();
_graphicsBuffer = null;
_targetBuffer?.Dispose();
_targetBuffer = null;
}
#endregion
#region Private Methods
// Unity is weird, so we need to wait 2 frames before we can get the graphics buffer
private void SetupGraphicsBuffer()
{
Mesh mesh = _mainMesh.sharedMesh;
Mesh shadowMesh = _shadowMesh.GetComponent<MeshFilter>().mesh;
_bufferLayout = 0;
if (mesh.HasVertexAttribute(VertexAttribute.Position)) _bufferLayout += 3;
if (mesh.HasVertexAttribute(VertexAttribute.Normal)) _bufferLayout += 3;
if (mesh.HasVertexAttribute(VertexAttribute.Tangent)) _bufferLayout += 4;
_bufferLayout *= 4; // 4 bytes per float
const float xThreadGroups = 64f;
_threadGroups = Mathf.CeilToInt(mesh.vertexCount / xThreadGroups);
_mainMesh.vertexBufferTarget |= GraphicsBuffer.Target.Raw;
shadowMesh.vertexBufferTarget |= GraphicsBuffer.Target.Raw;
_targetBuffer = shadowMesh.GetVertexBuffer(0);
//Debug.Log($"Initialized! BufferLayout: {_bufferLayout}, GraphicsBuffer: {_graphicsBuffer != null}, TargetBuffer: {_targetBuffer != null}");
}
private void ResetShadowClone()
{
_shadowMesh.shadowCastingMode = ShadowCastingMode.ShadowsOnly;
_shadowMesh.forceRenderingOff = !_shouldCastShadows;
// shadow casting needs clone to have original materials (uv discard)
// we also want to respect material swaps... but this is fucking slow :(
if (!ShadowCloneManager.s_CopyMaterialsToShadow)
return;
if (_hasShadowMaterials)
{
_shadowMesh.sharedMaterials = _mainMesh.sharedMaterials;
_hasShadowMaterials = false;
}
UpdateCloneMaterialProperties();
}
private void ConfigureShadowCloneForUiCulling()
{
_shadowMesh.shadowCastingMode = ShadowCastingMode.On;
_shadowMesh.forceRenderingOff = false;
// UI culling needs clone to have write-to-depth shader
if (_hasShadowMaterials) return;
_shadowMesh.sharedMaterials = _shadowMaterials;
_hasShadowMaterials = true;
// Not needed- MaterialPropertyBlock applied to renderer in RenderForShadow
//UpdateCloneMaterialProperties();
}
private void RenderShadowClone()
{
// thanks sdraw, i suck at matrix math
Matrix4x4 rootMatrix = _mainMesh.localToWorldMatrix.inverse * Matrix4x4.TRS(_rootBone.position, _rootBone.rotation, Vector3.one);
_graphicsBuffer = _mainMesh.GetVertexBuffer();
ShadowCloneHelper.shader.SetMatrix(s_SourceRootMatrix, rootMatrix);
ShadowCloneHelper.shader.SetBuffer(0, s_SourceBufferId, _graphicsBuffer);
ShadowCloneHelper.shader.SetBuffer(0, s_TargetBufferId, _targetBuffer);
ShadowCloneHelper.shader.SetInt(s_SourceBufferLayoutId, _bufferLayout);
ShadowCloneHelper.shader.Dispatch(0, _threadGroups, 1, 1);
_graphicsBuffer.Release();
}
private void UpdateCloneMaterialProperties()
{
// copy material properties to shadow clone materials
_mainMesh.GetPropertyBlock(_shadowMaterialBlock);
_shadowMesh.SetPropertyBlock(_shadowMaterialBlock);
}
#endregion
}

View file

@ -0,0 +1,217 @@
using System.Collections.Generic;
using ABI_RC.Core;
using ABI_RC.Core.Player;
using ABI_RC.Core.Savior;
using MagicaCloth;
using UnityEngine;
using UnityEngine.Rendering;
namespace NAK.BetterShadowClone;
public class ShadowCloneManager : MonoBehaviour
{
#region Singleton Implementation
private static ShadowCloneManager _instance;
public static ShadowCloneManager Instance
{
get
{
if (_instance != null) return _instance;
_instance = new GameObject("NAK.ShadowCloneManager").AddComponent<ShadowCloneManager>();
DontDestroyOnLoad(_instance.gameObject);
return _instance;
}
}
#endregion
private const string ShadowClonePostfix = "_ShadowClone";
//public const string CVRIgnoreForUiCulling = "CVRIgnoreForUiCulling"; // TODO: Shader Tag to ignore for UI culling?
// Game cameras
private static Camera s_MainCamera;
private static Camera s_UiCamera;
// Settings
internal static bool s_CopyMaterialsToShadow = true;
private static bool s_UseShadowToCullUi;
private const string ShadowCullUiSettingName = "ExperimentalAvatarOverrenderUI";
// Implementation
private bool _hasRenderedThisFrame;
// Shadow Clones
private readonly List<IShadowClone> s_ShadowClones = new();
public void AddShadowClone(IShadowClone clone)
=> s_ShadowClones.Add(clone);
// Debug
private bool _debugShadowProcessingTime;
private readonly StopWatch _stopWatch = new();
#region Unity Events
private void Start()
{
if (Instance != null
&& Instance != this)
{
Destroy(this);
return;
}
UpdatePlayerCameras();
s_CopyMaterialsToShadow = ModSettings.EntryCopyMaterialToShadow.Value;
s_UseShadowToCullUi = MetaPort.Instance.settings.GetSettingsBool(ShadowCullUiSettingName);
MetaPort.Instance.settings.settingBoolChanged.AddListener(OnSettingsBoolChanged);
}
private void OnEnable()
=> Camera.onPreCull += MyOnPreCull;
private void OnDisable()
=> Camera.onPreCull -= MyOnPreCull;
private void OnDestroy()
{
MetaPort.Instance.settings.settingBoolChanged.RemoveListener(OnSettingsBoolChanged);
}
#endregion
#region Shadow Clone Managment
private void Update()
{
_hasRenderedThisFrame = false;
}
private void MyOnPreCull(Camera cam)
{
bool forceRenderForUiCull = s_UseShadowToCullUi && cam == s_UiCamera;
if (_hasRenderedThisFrame && !forceRenderForUiCull)
return;
_hasRenderedThisFrame = true;
_stopWatch.Start();
for (int i = s_ShadowClones.Count - 1; i >= 0; i--)
{
IShadowClone clone = s_ShadowClones[i];
if (clone is not { IsValid: true })
{
clone?.Dispose();
s_ShadowClones.RemoveAt(i);
continue; // invalid or dead
}
if (!clone.Process()) continue; // not ready yet or disabled
if (forceRenderForUiCull)
clone.RenderForUiCulling(); // last cam to render
else
clone.RenderForShadow(); // first cam to render
}
_stopWatch.Stop();
if (_debugShadowProcessingTime) Debug.Log($"ShadowCloneManager.MyOnPreCull({forceRenderForUiCull}) took {_stopWatch.ElapsedMilliseconds}ms");
}
#endregion
#region Game Events
public void OnAvatarCleared()
{
// Dispose all shadow clones BEFORE game unloads avatar
// Otherwise we memory leak the shadow clones mesh & material instances!!!
foreach (IShadowClone clone in s_ShadowClones)
clone.Dispose();
s_ShadowClones.Clear();
}
private void OnSettingsBoolChanged(string settingName, bool settingValue)
{
if (settingName == ShadowCullUiSettingName)
s_UseShadowToCullUi = settingValue;
}
private void OnVRModeSwitchCompleted(bool _, Camera __)
{
UpdatePlayerCameras();
}
#endregion
#region Private Methods
private static void UpdatePlayerCameras()
{
s_MainCamera = PlayerSetup.Instance.GetActiveCamera().GetComponent<Camera>();
s_UiCamera = s_MainCamera.transform.Find("_UICamera").GetComponent<Camera>();
//s_PortableCamera = PortableCamera.Instance.cameraComponent;
}
#endregion
#region Static Helpers
internal static IShadowClone CreateShadowClone(Renderer renderer)
{
return renderer switch
{
SkinnedMeshRenderer skinnedMeshRenderer => new SkinnedShadowClone(skinnedMeshRenderer),
MeshRenderer meshRenderer => new MeshShadowClone(meshRenderer),
_ => null
};
}
internal static (MeshRenderer, MeshFilter) InstantiateShadowClone(SkinnedMeshRenderer meshRenderer)
{
GameObject shadowClone = new (meshRenderer.name + ShadowClonePostfix) { layer = CVRLayers.PlayerClone };
shadowClone.transform.SetParent(meshRenderer.transform, false);
shadowClone.transform.SetLocalPositionAndRotation(Vector3.zero, Quaternion.identity);
shadowClone.transform.localScale = Vector3.one;
MeshRenderer newMesh = shadowClone.AddComponent<MeshRenderer>();
MeshFilter newMeshFilter = shadowClone.AddComponent<MeshFilter>();
ShadowCloneHelper.ConfigureRenderer(newMesh, true);
// only shadow clone should cast shadows
newMesh.shadowCastingMode = ShadowCastingMode.ShadowsOnly;
// copy mesh and materials
newMeshFilter.sharedMesh = meshRenderer.sharedMesh;
newMesh.sharedMaterials = meshRenderer.sharedMaterials;
return (newMesh, newMeshFilter);
}
internal static (MeshRenderer, MeshFilter) InstantiateShadowClone(MeshRenderer meshRenderer)
{
GameObject shadowClone = new (meshRenderer.name + ShadowClonePostfix) { layer = CVRLayers.PlayerClone };
shadowClone.transform.SetParent(meshRenderer.transform, false);
shadowClone.transform.SetLocalPositionAndRotation(Vector3.zero, Quaternion.identity);
shadowClone.transform.localScale = Vector3.one;
MeshRenderer newMesh = shadowClone.AddComponent<MeshRenderer>();
MeshFilter newMeshFilter = shadowClone.AddComponent<MeshFilter>();
ShadowCloneHelper.ConfigureRenderer(newMesh, true);
// only shadow clone should cast shadows
newMesh.shadowCastingMode = ShadowCastingMode.ShadowsOnly;
// copy mesh and materials
newMeshFilter.sharedMesh = meshRenderer.GetComponent<MeshFilter>().sharedMesh;
newMesh.sharedMaterials = meshRenderer.sharedMaterials;
return (newMesh, newMeshFilter);
}
#endregion
}

View file

@ -0,0 +1,142 @@
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<Animator>();
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<Renderer>(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<Renderer> renderers, Transform root, Transform headBone)
{
var exclusions = CollectExclusions2(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);
}
TransformHiderManager.CreateTransformHider(renderer, exclusions);
}
}
#endregion
#region FPR Exclusion Processing
private static Dictionary<Transform, FPRExclusion> CollectExclusions2(Transform root, Transform headBone)
{
// add an fpr exclusion to the head bone
headBone.gameObject.AddComponent<FPRExclusion>().target = headBone;
// get all FPRExclusions
var fprExclusions = root.GetComponentsInChildren<FPRExclusion>(true).ToList();
// get all valid exclusion targets, and destroy invalid exclusions
Dictionary<Transform, FPRExclusion> exclusionTargetRoots = new();
for (int i = fprExclusions.Count - 1; i >= 0; i--)
{
FPRExclusion exclusion = fprExclusions[i];
if (exclusion.target == null)
{
Object.Destroy(exclusion);
continue;
}
exclusionTargetRoots.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 exclusionTargetRoots;
void ProcessExclusion(FPRExclusion exclusion, Transform transform)
{
if (exclusionTargetRoots.ContainsKey(transform)
&& exclusionTargetRoots[transform] != exclusion) return; // found other exclusion root
exclusion.affectedChildren.Add(transform); // associate with the exclusion
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
}

View file

@ -0,0 +1,30 @@
using UnityEngine;
namespace NAK.BetterShadowClone;
/// <summary>
/// Manual exclusion component for the TransformHider (FPR) system.
/// Allows you to manually hide and show a transform that would otherwise be hidden.
/// </summary>
public class FPRExclusion : MonoBehaviour
{
public Transform target;
internal List<Transform> affectedChildren = new();
[NonSerialized]
internal ITransformHider[] relevantHiders;
private void OnEnable()
=> SetFPRState(true);
private void OnDisable()
=> SetFPRState(false);
private void SetFPRState(bool state)
{
if (relevantHiders == null) return; // no hiders to set
foreach (ITransformHider hider in relevantHiders)
hider.IsActive = state;
}
}

View file

@ -0,0 +1,11 @@
namespace NAK.BetterShadowClone;
public interface ITransformHider : IDisposable
{
bool IsActive { get; set; }
bool IsValid { get; }
bool Process();
bool PostProcess();
void HideTransform();
void ShowTransform();
}

View file

@ -0,0 +1,86 @@
using System;
using UnityEngine;
using UnityEngine.Rendering;
namespace NAK.BetterShadowClone;
public class MeshTransformHider : ITransformHider
{
// lame 2 frame init stuff
private const int FrameInitCount = 0;
private int _frameInitCounter;
private bool _hasInitialized;
private bool _markedForDeath;
// mesh
private readonly MeshRenderer _mainMesh;
private bool _enabledState;
#region ITransformHider Methods
public bool IsActive { get; set; } = true; // default hide, but FPRExclusion can override
// anything player can touch is suspect to death
public bool IsValid => _mainMesh != null && !_markedForDeath;
public MeshTransformHider(MeshRenderer renderer)
{
_mainMesh = renderer;
if (_mainMesh == null
|| _mainMesh.sharedMaterials == null
|| _mainMesh.sharedMaterials.Length == 0)
{
Dispose();
}
}
public bool Process()
{
bool shouldRender = _mainMesh.enabled && _mainMesh.gameObject.activeInHierarchy;
// GraphicsBuffer becomes stale when mesh is disabled
if (!shouldRender)
{
_frameInitCounter = 0;
_hasInitialized = false;
return false;
}
// Unity is weird, so we need to wait 2 frames before we can get the graphics buffer
if (_frameInitCounter >= FrameInitCount)
{
if (_hasInitialized)
return true;
_hasInitialized = true;
return true;
}
_frameInitCounter++;
return false;
}
public bool PostProcess()
{
return true;
}
public void HideTransform()
{
_enabledState = _mainMesh.enabled;
_mainMesh.enabled = false;
}
public void ShowTransform()
{
_mainMesh.enabled = _enabledState;
}
public void Dispose()
{
_markedForDeath = true;
}
#endregion
}

View file

@ -0,0 +1,191 @@
using System;
using UnityEngine;
using UnityEngine.Rendering;
namespace NAK.BetterShadowClone;
public class SkinnedTransformHider : ITransformHider
{
private static readonly int s_Pos = Shader.PropertyToID("pos");
private static readonly int s_BufferLayout = Shader.PropertyToID("bufferLayout");
private static readonly int s_WeightedCount = Shader.PropertyToID("weightedCount");
private static readonly int s_WeightedVertices = Shader.PropertyToID("weightedVertices");
private static readonly int s_VertexBuffer = Shader.PropertyToID("VertexBuffer");
// lame 2 frame init stuff
private const int FrameInitCount = 0;
private int _frameInitCounter;
private bool _hasInitialized;
private bool _markedForDeath;
// mesh & bone
private readonly Transform _shrinkBone;
private readonly SkinnedMeshRenderer _mainMesh;
private readonly Transform _rootBone;
// exclusion
private readonly FPRExclusion _exclusion;
// hider stuff
private GraphicsBuffer _graphicsBuffer;
private int _bufferLayout;
private ComputeBuffer _computeBuffer;
private int _vertexCount;
private int _threadGroups;
#region ITransformHider Methods
public bool IsActive { get; set; } = true; // default hide, but FPRExclusion can override
// anything player can touch is suspect to death
public bool IsValid => _mainMesh != null && _shrinkBone != null && !_markedForDeath;
public SkinnedTransformHider(SkinnedMeshRenderer renderer, FPRExclusion exclusion)
{
_mainMesh = renderer;
_shrinkBone = exclusion.target;
_exclusion = exclusion;
if (_exclusion == null
|| _shrinkBone == null
|| _mainMesh == null
|| _mainMesh.sharedMesh == null
|| _mainMesh.sharedMaterials == null
|| _mainMesh.sharedMaterials.Length == 0)
{
Dispose();
return; // no mesh or bone!
}
// find the head vertices
var exclusionVerts = FindExclusionVertList();
if (exclusionVerts.Count == 0)
{
Dispose();
return; // no head vertices!
}
_rootBone = _mainMesh.rootBone;
_rootBone ??= _mainMesh.transform; // fallback to transform if no root bone
_vertexCount = exclusionVerts.Count;
_computeBuffer = new ComputeBuffer(_vertexCount, sizeof(int));
_computeBuffer.SetData(exclusionVerts.ToArray());
}
public bool Process()
{
bool shouldRender = _mainMesh.enabled && _mainMesh.gameObject.activeInHierarchy;
// GraphicsBuffer becomes stale when mesh is disabled
if (!shouldRender)
{
_frameInitCounter = 0;
_hasInitialized = false;
return false;
}
// Unity is weird, so we need to wait 2 frames before we can get the graphics buffer
if (_frameInitCounter >= FrameInitCount)
{
if (_hasInitialized)
return true;
_hasInitialized = true;
SetupGraphicsBuffer();
return true;
}
_mainMesh.forceRenderingOff = true; // force off if mesh is disabled
_frameInitCounter++;
return false;
}
public bool PostProcess()
{
return false; // not needed
}
public void HideTransform()
{
_mainMesh.forceRenderingOff = false;
// probably fine
Vector3 pos = _rootBone.transform.InverseTransformPoint(_shrinkBone.position) * _rootBone.lossyScale.y;
_graphicsBuffer = _mainMesh.GetVertexBuffer();
TransformHiderManager.shader.SetVector(s_Pos, pos);
TransformHiderManager.shader.SetInt(s_WeightedCount, _vertexCount);
TransformHiderManager.shader.SetInt(s_BufferLayout, _bufferLayout);
TransformHiderManager.shader.SetBuffer(0, s_WeightedVertices, _computeBuffer);
TransformHiderManager.shader.SetBuffer(0, s_VertexBuffer, _graphicsBuffer);
TransformHiderManager.shader.Dispatch(0, _threadGroups, 1, 1);
_graphicsBuffer.Release();
}
public void ShowTransform()
{
// not needed
}
public void Dispose()
{
_markedForDeath = true;
_graphicsBuffer?.Dispose();
_graphicsBuffer = null;
_computeBuffer?.Dispose();
_computeBuffer = null;
}
#endregion
#region Private Methods
// Unity is weird, so we need to wait 2 frames before we can get the graphics buffer
private void SetupGraphicsBuffer()
{
Mesh mesh = _mainMesh.sharedMesh;
_bufferLayout = 0;
if (mesh.HasVertexAttribute(VertexAttribute.Position)) _bufferLayout += 3;
if (mesh.HasVertexAttribute(VertexAttribute.Normal)) _bufferLayout += 3;
if (mesh.HasVertexAttribute(VertexAttribute.Tangent)) _bufferLayout += 4;
// ComputeShader is doing bitshift so we dont need to multiply by 4
//_bufferLayout *= 4; // 4 bytes per float
const float xThreadGroups = 64f;
_threadGroups = Mathf.CeilToInt(mesh.vertexCount / xThreadGroups);
_mainMesh.vertexBufferTarget |= GraphicsBuffer.Target.Raw;
}
private List<int> FindExclusionVertList()
{
var boneWeights = _mainMesh.sharedMesh.boneWeights;
var bones = _exclusion.affectedChildren;
HashSet<int> weights = new(); //get indexs of child bones
for (int i = 0; i < _mainMesh.bones.Length; i++)
if (bones.Contains(_mainMesh.bones[i])) weights.Add(i);
List<int> headVertices = new();
for (int i = 0; i < boneWeights.Length; i++)
{
BoneWeight weight = boneWeights[i];
const float minWeightThreshold = 0.2f;
if (weights.Contains(weight.boneIndex0) && weight.weight0 > minWeightThreshold
|| weights.Contains(weight.boneIndex1) && weight.weight1 > minWeightThreshold
|| weights.Contains(weight.boneIndex2) && weight.weight2 > minWeightThreshold
|| weights.Contains(weight.boneIndex3) && weight.weight3 > minWeightThreshold)
headVertices.Add(i);
}
return headVertices;
}
#endregion
}

View file

@ -0,0 +1,237 @@
using System.Collections.Generic;
using ABI_RC.Core.Player;
using ABI_RC.Core.Savior;
using ABI_RC.Systems.Camera;
using MagicaCloth;
using UnityEngine;
namespace NAK.BetterShadowClone;
// Built on top of Koneko's BoneHider but to mimic the ShadowCloneManager
public class TransformHiderManager : MonoBehaviour
{
public static ComputeShader shader;
#region Singleton Implementation
private static TransformHiderManager _instance;
public static TransformHiderManager Instance
{
get
{
if (_instance != null) return _instance;
_instance = new GameObject("Koneko.TransformHiderManager").AddComponent<TransformHiderManager>();
DontDestroyOnLoad(_instance.gameObject);
return _instance;
}
}
#endregion
// Game cameras
private static Camera s_MainCamera;
private static Camera s_UiCamera;
// Settings
internal static bool s_DebugHeadHide;
// Implementation
private bool _hasRenderedThisFrame;
// Shadow Clones
private readonly List<ITransformHider> s_TransformHider = new();
public void AddTransformHider(ITransformHider clone)
=> s_TransformHider.Add(clone);
// Debug
private bool _debugHeadHiderProcessingTime;
private readonly StopWatch _stopWatch = new();
#region Unity Events
private void Start()
{
if (Instance != null
&& Instance != this)
{
Destroy(this);
return;
}
UpdatePlayerCameras();
s_DebugHeadHide = ModSettings.EntryDebugHeadHide.Value;
}
private void OnEnable()
{
Camera.onPreRender += MyOnPreRender;
Camera.onPostRender += MyOnPostRender;
}
private void OnDisable()
{
Camera.onPreRender -= MyOnPreRender;
Camera.onPostRender -= MyOnPostRender;
}
#endregion
#region Transform Hider Managment
private void Update()
{
_hasRenderedThisFrame = false;
}
private void MyOnPreRender(Camera cam)
{
if (_hasRenderedThisFrame)
return; // can only hide head once per frame
if (cam != s_MainCamera // only hide in player cam, or if debug is on
&& !s_DebugHeadHide)
return;
if (!CheckPlayerCamWithinRange())
return; // player is too far away (likely HoloPort or Sitting)
if (!ShadowCloneMod.CheckWantsToHideHead(cam))
return; // listener said no (Third Person, etc)
_hasRenderedThisFrame = true;
_stopWatch.Start();
for (int i = s_TransformHider.Count - 1; i >= 0; i--)
{
ITransformHider hider = s_TransformHider[i];
if (hider is not { IsValid: true })
{
hider?.Dispose();
s_TransformHider.RemoveAt(i);
continue; // invalid or dead
}
if (!hider.Process()) continue; // not ready yet or disabled
if (hider.IsActive) hider.HideTransform();
}
_stopWatch.Stop();
if (_debugHeadHiderProcessingTime) Debug.Log($"TransformHiderManager.MyOnPreRender({s_DebugHeadHide}) took {_stopWatch.ElapsedMilliseconds}ms");
}
private void MyOnPostRender(Camera cam)
{
if (cam != s_UiCamera) return; // ui camera is expected to render last
for (int i = s_TransformHider.Count - 1; i >= 0; i--)
{
ITransformHider hider = s_TransformHider[i];
if (hider is not { IsValid: true })
{
hider?.Dispose();
s_TransformHider.RemoveAt(i);
continue; // invalid or dead
}
if (!hider.PostProcess()) continue; // does not need post processing
if (hider.IsActive) hider.ShowTransform();
}
}
#endregion
#region Game Events
public void OnAvatarCleared()
{
// Dispose all shadow clones BEFORE game unloads avatar
// Otherwise we memory leak the shadow clones mesh & material instances!!!
foreach (ITransformHider hider in s_TransformHider)
hider.Dispose();
s_TransformHider.Clear();
}
private void OnVRModeSwitchCompleted(bool _, Camera __)
{
UpdatePlayerCameras();
}
#endregion
#region Private Methods
private static void UpdatePlayerCameras()
{
s_MainCamera = PlayerSetup.Instance.GetActiveCamera().GetComponent<Camera>();
s_UiCamera = s_MainCamera.transform.Find("_UICamera").GetComponent<Camera>();
}
private static bool CheckPlayerCamWithinRange()
{
if (PlayerSetup.Instance == null)
return false; // hack
const float MinHeadHidingRange = 0.5f;
Vector3 playerHeadPos = PlayerSetup.Instance.GetViewWorldPosition();
Vector3 playerCamPos = s_MainCamera.transform.position;
float scaleModifier = PlayerSetup.Instance.GetPlaySpaceScale();
return (Vector3.Distance(playerHeadPos, playerCamPos) < (MinHeadHidingRange * scaleModifier));
}
#endregion
#region Static Helpers
internal static bool IsLegacyFPRExcluded(Component renderer)
=> renderer.gameObject.name.Contains("[FPR]");
internal static ITransformHider CreateTransformHider(Component renderer, Transform bone)
{
if (IsLegacyFPRExcluded(renderer))
return null;
return renderer switch
{
//SkinnedMeshRenderer skinnedMeshRenderer => new SkinnedTransformHider(skinnedMeshRenderer, bone),
MeshRenderer meshRenderer => new MeshTransformHider(meshRenderer),
_ => null
};
}
internal static void CreateTransformHider(Component renderer, Dictionary<Transform, FPRExclusion> exclusions)
{
if (IsLegacyFPRExcluded(renderer))
return;
if (renderer is SkinnedMeshRenderer skinnedMeshRenderer)
{
// get all bones for renderer
var bones = skinnedMeshRenderer.bones;
List<FPRExclusion> fprExclusions = new();
// check if any bones are excluded
foreach (Transform bone in bones)
{
if (!exclusions.TryGetValue(bone, out FPRExclusion exclusion))
continue;
fprExclusions.Add(exclusion);
}
foreach (FPRExclusion exclusion in fprExclusions)
{
ITransformHider hider = new SkinnedTransformHider(skinnedMeshRenderer, exclusion);
Instance.AddTransformHider(hider);
}
}
return;
}
#endregion
}

View file

@ -0,0 +1,23 @@
{
"_id": -1,
"name": "EzCurls",
"modversion": "1.0.0",
"gameversion": "2023r173",
"loaderversion": "0.6.1",
"modtype": "Mod",
"author": "NotAKidoS",
"description": "A mod that allows you to tune your finger curls to your liking. Supposedly can help with VR sign language, as raw finger curls are not that great for quick and precise gestures.\n\nThe settings are not too coherent, it is mostly a bunch of things thrown at the wall, but it works for me. I hope it works for you too.",
"searchtags": [
"curls",
"fingers",
"index",
"knuckles"
],
"requirements": [
"UIExpansionKit"
],
"downloadlink": "https://github.com/NotAKidOnSteam/NAK_CVR_Mods/releases/download/r24/EzCurls.dll",
"sourcelink": "https://github.com/NotAKidOnSteam/NAK_CVR_Mods/tree/main/EzCurls/",
"changelog": "- Initial CVRMG release",
"embedcolor": "7d7d7d"
}