NSNzBF`)5bT< z6~`0*KewXpTe*;x0{pz|juw!JCqW7c*#(!q)L-ZU66+b(lDZI)TD962@;4k2>YYc{ zQa-y?3~B>~)+^zI`o&&>4~nKpx{H5wgW)`j zhM!=ay?lbcA}+oxbBMt%cB%aOQlG} s>4#1(=wvS^ nEwPis8vKn71CZJ^>X!Ze$W1t+I4oM7&hv#d{;teH)epR)u# z@o+y?>A!NZg6h_1zf9fyM9mVgEX{27ZBOUA%L*2C(&K($u>xNgqZt7osV_cn^+!n} zWv==*s<6QT%n{snO^@l)ppKG#5=cF}Ks5Y~U_|U9I2MG)vWP2ogSa|l&6ozVJ7)p; zEmSRy!-+>#=Ym{C_Mc~4LP1u#dzQU_(6!dVYUL~o^whbcFOsi7wV3|5WJAxd$7Si= z-+!Ru2BOGprfOK!wduii%xM)jdSpdGi?aDy%oEh{Sn+GclL9ur$uiaN6#Pa@%q9v` z$^CHZ#{bh|o$_lIO~|xtajPiMFW6jt3 1Z*SFT2wPo9u z77PS3wW0B65C@Wdidrh_VGRQO2?PU)EvFK5^;k<|B0rGOC-?UuJAq|r`m)y?mymV7 zG!0Jn0KxupCHIQuFweJn7az__SHT)N)Jr(vjJkM$ZlupxwnA(7hj!$&*6vt~^Gl7l zC{+52HpD~oWOJ0E?N8_0M}j&80ji0y@h|;PF1MX^BGLG`%P)v62ig;>dB>I9px|Kr z@vVA57+Fzfj7(OE$D 8_=DdrRy^(?j3@xg zBx)P}s^k;HPD{HRHB#{hNVgXBm79;96q8`zO_bxY GdAJt-G4E SO) zn*N+CAH*mUv%@sLQ{j%6aDnD5Z&YX{6z2yEl z0ZbtHdcF%vttk5!kEOs@E}VAz zi-}!@!A1Bv!Q{Q{=7iwQ4IZ IOL@{$wR-c
7}{HVV!PoE&6o`idF$Lv_`uP;Je8SKRsuUXg_Y9mvK{zxNGomTGn1`Ec8 z-rA{ZnQ1-&0?GSzNEFpdxR!EO{W(*bcL+f0Wr7mM3DSGIT?Z1d2`#VsVSKYx5RtCo z-BQCjsWwUH4pSo-n$gm8oto|@en`vFCzB{X(%>lvqUfPKLB8mUFTKH7eA Ei7A {DlHS<;sidSZ5*>)Yn+2#Ov&)l^{+>VNHDqdJ5XUroQd z@8zvyf%pNyn>5j!Y{hKo0QO*pt)ltJDH*IvT^dYBsFa3 IGO0~a)pc8Y>9W;%V>D6& Xzmy29udSO(taj=*6R@}c{xlMt`RPha literal 0 HcmV?d00001 diff --git a/BetterShadowClone/ShadowClone/IShadowClone/IShadowClone.cs b/BetterShadowClone/ShadowClone/IShadowClone/IShadowClone.cs new file mode 100644 index 0000000..7d121b2 --- /dev/null +++ b/BetterShadowClone/ShadowClone/IShadowClone/IShadowClone.cs @@ -0,0 +1,11 @@ +using System; + +namespace NAK.BetterShadowClone; + +public interface IShadowClone : IDisposable +{ + bool IsValid { get; } + bool Process(); + void RenderForShadow(); + void RenderForUiCulling(); +} \ No newline at end of file diff --git a/BetterShadowClone/ShadowClone/IShadowClone/MeshShadowClone.cs b/BetterShadowClone/ShadowClone/IShadowClone/MeshShadowClone.cs new file mode 100644 index 0000000..f2670bd --- /dev/null +++ b/BetterShadowClone/ShadowClone/IShadowClone/MeshShadowClone.cs @@ -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 (); + + 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 +} \ No newline at end of file diff --git a/BetterShadowClone/ShadowClone/IShadowClone/SkinnedShadowClone.cs b/BetterShadowClone/ShadowClone/IShadowClone/SkinnedShadowClone.cs new file mode 100644 index 0000000..70fbdc1 --- /dev/null +++ b/BetterShadowClone/ShadowClone/IShadowClone/SkinnedShadowClone.cs @@ -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 ().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 +} \ No newline at end of file diff --git a/BetterShadowClone/ShadowClone/ShadowCloneManager.cs b/BetterShadowClone/ShadowClone/ShadowCloneManager.cs new file mode 100644 index 0000000..a8f6706 --- /dev/null +++ b/BetterShadowClone/ShadowClone/ShadowCloneManager.cs @@ -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 (); + 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 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 (); + s_UiCamera = s_MainCamera.transform.Find("_UICamera").GetComponent (); + //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 (); + MeshFilter newMeshFilter = shadowClone.AddComponent (); + + 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 (); + MeshFilter newMeshFilter = shadowClone.AddComponent (); + + ShadowCloneHelper.ConfigureRenderer(newMesh, true); + + // only shadow clone should cast shadows + newMesh.shadowCastingMode = ShadowCastingMode.ShadowsOnly; + + // copy mesh and materials + newMeshFilter.sharedMesh = meshRenderer.GetComponent ().sharedMesh; + newMesh.sharedMaterials = meshRenderer.sharedMaterials; + + return (newMesh, newMeshFilter); + } + + #endregion +} \ No newline at end of file diff --git a/BetterShadowClone/ShadowCloneHelper.cs b/BetterShadowClone/ShadowCloneHelper.cs new file mode 100644 index 0000000..6b66017 --- /dev/null +++ b/BetterShadowClone/ShadowCloneHelper.cs @@ -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 (); + if (animator == null || animator.avatar == null || animator.avatar.isHuman == false) + { + ShadowCloneMod.Logger.Warning("Avatar is not humanoid!"); + return; + } + + Transform headBone = animator.GetBoneTransform(HumanBodyBones.Head); + if (headBone == null) + { + ShadowCloneMod.Logger.Warning("Head bone not found!"); + return; + } + + var renderers = avatar.GetComponentsInChildren (true); + if (renderers == null || renderers.Length == 0) + { + ShadowCloneMod.Logger.Warning("No renderers found!"); + return; + } + + // create shadow clones + ProcessRenderers(renderers, avatar.transform, headBone); + } + + private static void ProcessRenderers(IEnumerable renderers, Transform root, Transform headBone) + { + 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 CollectExclusions2(Transform root, Transform headBone) + { + // add an fpr exclusion to the head bone + headBone.gameObject.AddComponent ().target = headBone; + + // get all FPRExclusions + var fprExclusions = root.GetComponentsInChildren (true).ToList(); + + // get all valid exclusion targets, and destroy invalid exclusions + Dictionary 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 +} \ No newline at end of file diff --git a/BetterShadowClone/TransformHider/FPRExclusion.cs b/BetterShadowClone/TransformHider/FPRExclusion.cs new file mode 100644 index 0000000..7c713f5 --- /dev/null +++ b/BetterShadowClone/TransformHider/FPRExclusion.cs @@ -0,0 +1,30 @@ +using UnityEngine; + +namespace NAK.BetterShadowClone; + +/// +/// Manual exclusion component for the TransformHider (FPR) system. +/// Allows you to manually hide and show a transform that would otherwise be hidden. +/// +public class FPRExclusion : MonoBehaviour +{ + public Transform target; + + internal ListaffectedChildren = 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; + } +} \ No newline at end of file diff --git a/BetterShadowClone/TransformHider/ITransformHider/ITransformHider.cs b/BetterShadowClone/TransformHider/ITransformHider/ITransformHider.cs new file mode 100644 index 0000000..cd4a94a --- /dev/null +++ b/BetterShadowClone/TransformHider/ITransformHider/ITransformHider.cs @@ -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(); +} \ No newline at end of file diff --git a/BetterShadowClone/TransformHider/ITransformHider/MeshTransformHider.cs b/BetterShadowClone/TransformHider/ITransformHider/MeshTransformHider.cs new file mode 100644 index 0000000..69e174f --- /dev/null +++ b/BetterShadowClone/TransformHider/ITransformHider/MeshTransformHider.cs @@ -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 +} \ No newline at end of file diff --git a/BetterShadowClone/TransformHider/ITransformHider/SkinnedTransformHider.cs b/BetterShadowClone/TransformHider/ITransformHider/SkinnedTransformHider.cs new file mode 100644 index 0000000..fc0f285 --- /dev/null +++ b/BetterShadowClone/TransformHider/ITransformHider/SkinnedTransformHider.cs @@ -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 FindExclusionVertList() + { + var boneWeights = _mainMesh.sharedMesh.boneWeights; + var bones = _exclusion.affectedChildren; + + HashSet 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 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 +} \ No newline at end of file diff --git a/BetterShadowClone/TransformHider/TransformHiderManager.cs b/BetterShadowClone/TransformHider/TransformHiderManager.cs new file mode 100644 index 0000000..13040e0 --- /dev/null +++ b/BetterShadowClone/TransformHider/TransformHiderManager.cs @@ -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 (); + 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 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 (); + s_UiCamera = s_MainCamera.transform.Find("_UICamera").GetComponent (); + } + + 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 exclusions) + { + if (IsLegacyFPRExcluded(renderer)) + return; + + if (renderer is SkinnedMeshRenderer skinnedMeshRenderer) + { + // get all bones for renderer + var bones = skinnedMeshRenderer.bones; + List 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 +} \ No newline at end of file diff --git a/BetterShadowClone/format.json b/BetterShadowClone/format.json new file mode 100644 index 0000000..6a8d72c --- /dev/null +++ b/BetterShadowClone/format.json @@ -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" +} \ No newline at end of file