mirror of
https://github.com/NotAKidoS/NAK_CVR_Mods.git
synced 2025-09-02 14:29:25 +00:00
[BetterShadowClone] Throwing on git before i completely overcomplicate and have to revert lmao
This commit is contained in:
parent
d155ea546e
commit
5ee7dca50b
18 changed files with 1697 additions and 0 deletions
8
BetterShadowClone/BetterShadowClone.csproj
Normal file
8
BetterShadowClone/BetterShadowClone.csproj
Normal 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>
|
130
BetterShadowClone/Koneko/BoneHider.cs
Normal file
130
BetterShadowClone/Koneko/BoneHider.cs
Normal 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
144
BetterShadowClone/Main.cs
Normal 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
|
||||
}
|
45
BetterShadowClone/ModSettings.cs
Normal file
45
BetterShadowClone/ModSettings.cs
Normal 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;
|
||||
}
|
||||
}
|
29
BetterShadowClone/Properties/AssemblyInfo.cs
Normal file
29
BetterShadowClone/Properties/AssemblyInfo.cs
Normal 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";
|
||||
}
|
16
BetterShadowClone/README.md
Normal file
16
BetterShadowClone/README.md
Normal 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.
|
BIN
BetterShadowClone/Resources/bettershadowclone.assets
Normal file
BIN
BetterShadowClone/Resources/bettershadowclone.assets
Normal file
Binary file not shown.
11
BetterShadowClone/ShadowClone/IShadowClone/IShadowClone.cs
Normal file
11
BetterShadowClone/ShadowClone/IShadowClone/IShadowClone.cs
Normal file
|
@ -0,0 +1,11 @@
|
|||
using System;
|
||||
|
||||
namespace NAK.BetterShadowClone;
|
||||
|
||||
public interface IShadowClone : IDisposable
|
||||
{
|
||||
bool IsValid { get; }
|
||||
bool Process();
|
||||
void RenderForShadow();
|
||||
void RenderForUiCulling();
|
||||
}
|
154
BetterShadowClone/ShadowClone/IShadowClone/MeshShadowClone.cs
Normal file
154
BetterShadowClone/ShadowClone/IShadowClone/MeshShadowClone.cs
Normal 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
|
||||
}
|
223
BetterShadowClone/ShadowClone/IShadowClone/SkinnedShadowClone.cs
Normal file
223
BetterShadowClone/ShadowClone/IShadowClone/SkinnedShadowClone.cs
Normal 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
|
||||
}
|
217
BetterShadowClone/ShadowClone/ShadowCloneManager.cs
Normal file
217
BetterShadowClone/ShadowClone/ShadowCloneManager.cs
Normal 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
|
||||
}
|
142
BetterShadowClone/ShadowCloneHelper.cs
Normal file
142
BetterShadowClone/ShadowCloneHelper.cs
Normal 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
|
||||
}
|
30
BetterShadowClone/TransformHider/FPRExclusion.cs
Normal file
30
BetterShadowClone/TransformHider/FPRExclusion.cs
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
237
BetterShadowClone/TransformHider/TransformHiderManager.cs
Normal file
237
BetterShadowClone/TransformHider/TransformHiderManager.cs
Normal 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
|
||||
}
|
23
BetterShadowClone/format.json
Normal file
23
BetterShadowClone/format.json
Normal 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"
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue