From cd9ea221d267bef2b7b89aec39f5d5974be4b522 Mon Sep 17 00:00:00 2001 From: shiroblank Date: Wed, 17 Sep 2025 01:38:27 +0100 Subject: [PATCH] stolen code :D --- Properties/AssemblyInfo.cs | 21 + VRCLightVolumes/LVUtils.cs | 251 ++++++++ VRCLightVolumes/LightVolume.cs | 290 ++++++++++ VRCLightVolumes/LightVolumeData.cs | 18 + VRCLightVolumes/LightVolumeDataSorter.cs | 31 + VRCLightVolumes/LightVolumeInstance.cs | 143 +++++ VRCLightVolumes/LightVolumeManager.cs | 538 ++++++++++++++++++ VRCLightVolumes/LightVolumeSetup.cs | 335 +++++++++++ VRCLightVolumes/PointLightVolume.cs | 268 +++++++++ VRCLightVolumes/PointLightVolumeInstance.cs | 313 ++++++++++ red.sim.LightVolumesUdon.csproj | 94 +++ red.sim.LightVolumesUdon.sln | 21 + red.sim.LightVolumesUdon/Main.cs | 30 + .../Properties/AssemblyInfoParams.cs | 11 + .../System.Runtime.dll | Bin 0 -> 32256 bytes .../netstandard.dll | Bin 0 -> 91136 bytes 16 files changed, 2364 insertions(+) create mode 100644 Properties/AssemblyInfo.cs create mode 100644 VRCLightVolumes/LVUtils.cs create mode 100644 VRCLightVolumes/LightVolume.cs create mode 100644 VRCLightVolumes/LightVolumeData.cs create mode 100644 VRCLightVolumes/LightVolumeDataSorter.cs create mode 100644 VRCLightVolumes/LightVolumeInstance.cs create mode 100644 VRCLightVolumes/LightVolumeManager.cs create mode 100644 VRCLightVolumes/LightVolumeSetup.cs create mode 100644 VRCLightVolumes/PointLightVolume.cs create mode 100644 VRCLightVolumes/PointLightVolumeInstance.cs create mode 100644 red.sim.LightVolumesUdon.csproj create mode 100644 red.sim.LightVolumesUdon.sln create mode 100644 red.sim.LightVolumesUdon/Main.cs create mode 100644 red.sim.LightVolumesUdon/Properties/AssemblyInfoParams.cs create mode 100644 red.sim.LightVolumesUdonReferences/System.Runtime.dll create mode 100644 red.sim.LightVolumesUdonReferences/netstandard.dll diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..8a9fb74 --- /dev/null +++ b/Properties/AssemblyInfo.cs @@ -0,0 +1,21 @@ +using MelonLoader; +using red.sim.LightVolumesUdon; +using System.Diagnostics; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.Versioning; +using System.Security.Permissions; + +[assembly: AssemblyVersion("0.0.0.0")] +[assembly: CompilationRelaxations(8)] +[assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)] +[assembly: HarmonyDontPatchAll] +[assembly: MelonAuthorColor(0xff, 40, 144, 209)] +[assembly: MelonColor(0xff, 3, 252, 78)] +[assembly: MelonGame("Alpha Blend Interactive", "ChilloutVR")] +[assembly: MelonInfo(typeof(Main), "LightVolumesUdon", "1.0.0", "REDSIM , SketchFoxsky", "N/A")] +[assembly: MelonPlatform(,)] // JustDecompile was unable to locate the assembly where attribute parameters types are defined. Generating parameters values is impossible. +[assembly: MelonPlatformDomain(,)] // JustDecompile was unable to locate the assembly where attribute parameters types are defined. Generating parameters values is impossible. +[assembly: RuntimeCompatibility(WrapNonExceptionThrows=true)] +[assembly: SecurityPermission(, SkipVerification=true)] +[module: RefSafetyRules(11)] diff --git a/VRCLightVolumes/LVUtils.cs b/VRCLightVolumes/LVUtils.cs new file mode 100644 index 0000000..48f1bf5 --- /dev/null +++ b/VRCLightVolumes/LVUtils.cs @@ -0,0 +1,251 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using UnityEngine; +using UnityEngine.Rendering; + +namespace VRCLightVolumes +{ + public class LVUtils + { + public LVUtils() + { + } + + public static bool Apply3DTextureData(Texture3D texture, Color[] colors) + { + bool flag; + try + { + texture.SetPixels(colors); + texture.Apply(false); + flag = true; + } + catch (UnityException unityException) + { + Debug.LogError(String.Concat("[LightVolumeUtils] Failed to SetPixels in the Texture3D. Error: ", unityException.Message)); + flag = false; + } + return flag; + } + + public static Vector3[] BilateralDenoise3D(Vector3[] input, int w, int h, int d, float sigmaSpatial = 1f, float sigmaRange = 0.1f) + { + Vector3[] vector3Array = new Vector3[(int)input.Length]; + int num = Mathf.CeilToInt(2f * sigmaSpatial); + for (int i = 0; i < d; i++) + { + for (int j = 0; j < h; j++) + { + for (int k = 0; k < w; k++) + { + int num1 = k + j * w + i * w * h; + Vector3 vector3 = input[num1]; + Vector3 _zero = Vector3.get_zero(); + float single = 0f; + for (int l = -num; l <= num; l++) + { + for (int m = -num; m <= num; m++) + { + for (int n = -num; n <= num; n++) + { + int num2 = k + n; + int num3 = j + m; + int num4 = i + l; + if ((num2 < 0 || num3 < 0 || num4 < 0 || num2 >= w || num3 >= h ? false : num4 < d)) + { + int num5 = num2 + num3 * w + num4 * w * h; + Vector3 vector31 = input[num5]; + float single1 = (float)(n * n + m * m + l * l); + float _sqrMagnitude = (vector31 - vector3).get_sqrMagnitude(); + float single2 = Mathf.Exp(-single1 / (2f * sigmaSpatial * sigmaSpatial)); + float single3 = Mathf.Exp(-_sqrMagnitude / (2f * sigmaRange * sigmaRange)); + float single4 = single2 * single3; + _zero = _zero + (vector31 * single4); + single += single4; + } + } + } + } + vector3Array[num1] = (single > 0f ? _zero / single : vector3); + } + } + } + return vector3Array; + } + + public static Bounds BoundsFromTRS(Matrix4x4 trs) + { + Vector3 column = trs.GetColumn(3); + Vector3 vector3 = trs.GetColumn(0) * 0.5f; + Vector3 column1 = trs.GetColumn(1) * 0.5f; + Vector3 vector31 = trs.GetColumn(2) * 0.5f; + Vector3 vector32 = new Vector3(Mathf.Abs(vector3.x) + Mathf.Abs(column1.x) + Mathf.Abs(vector31.x), Mathf.Abs(vector3.y) + Mathf.Abs(column1.y) + Mathf.Abs(vector31.y), Mathf.Abs(vector3.z) + Mathf.Abs(column1.z) + Mathf.Abs(vector31.z)); + return new Bounds(column, vector32 * 2f); + } + + public static bool CheckSHL2(SphericalHarmonicsL2 sh) + { + bool flag; + int num = 0; + while (true) + { + if (num < 3) + { + int num1 = 4; + while (num1 < 9) + { + if (sh.get_Item(num, num1) == 0f) + { + num1++; + } + else + { + flag = true; + return flag; + } + } + num++; + } + else + { + flag = false; + break; + } + } + return flag; + } + + public static SphericalHarmonicsL2 DeringSH(SphericalHarmonicsL2 sh) + { + Vector3 vector3 = new Vector3(sh.get_Item(0, 0), sh.get_Item(1, 0), sh.get_Item(2, 0)); + Vector3 vector31 = new Vector3(sh.get_Item(0, 3), sh.get_Item(0, 1), sh.get_Item(0, 2)); + Vector3 vector32 = new Vector3(sh.get_Item(1, 3), sh.get_Item(1, 1), sh.get_Item(1, 2)); + Vector3 vector33 = new Vector3(sh.get_Item(2, 3), sh.get_Item(2, 1), sh.get_Item(2, 2)); + vector31 = LVUtils.DeringSingleSH(vector3.x, vector31); + vector32 = LVUtils.DeringSingleSH(vector3.y, vector32); + vector33 = LVUtils.DeringSingleSH(vector3.z, vector33); + sh.set_Item(0, 3, vector31.x); + sh.set_Item(0, 1, vector31.y); + sh.set_Item(0, 2, vector31.z); + sh.set_Item(1, 3, vector32.x); + sh.set_Item(1, 1, vector32.y); + sh.set_Item(1, 2, vector32.z); + sh.set_Item(2, 3, vector33.x); + sh.set_Item(2, 1, vector33.y); + sh.set_Item(2, 2, vector33.z); + return sh; + } + + public static Vector3 DeringSingleSH(float L0, Vector3 L1) + { + L1 *= 0.5f; + float _magnitude = L1.get_magnitude(); + if (((double)_magnitude <= 0 ? false : (double)L0 > 0)) + { + L1 *= Mathf.Min(L0 / _magnitude, 1.13f); + } + return L1; + } + + public static Mesh GenerateIcoSphere(float radius = 0.5f, int subdivisions = 2) + { + LVUtils.u003cu003ec__DisplayClass12_0 vector3s = new LVUtils.u003cu003ec__DisplayClass12_0(); + Vector3[] vector3 = new Vector3[] { new Vector3(-0.5257311f, 0.8506508f, 0f), new Vector3(0.5257311f, 0.8506508f, 0f), new Vector3(-0.5257311f, -0.8506508f, 0f), new Vector3(0.5257311f, -0.8506508f, 0f), new Vector3(0f, -0.5257311f, 0.8506508f), new Vector3(0f, 0.5257311f, 0.8506508f), new Vector3(0f, -0.5257311f, -0.8506508f), new Vector3(0f, 0.5257311f, -0.8506508f), new Vector3(0.8506508f, 0f, -0.5257311f), new Vector3(0.8506508f, 0f, 0.5257311f), new Vector3(-0.8506508f, 0f, -0.5257311f), new Vector3(-0.8506508f, 0f, 0.5257311f) }; + int[] numArray = new Int32[] { 0, 11, 5, 0, 5, 1, 0, 1, 7, 0, 7, 10, 0, 10, 11, 1, 5, 9, 5, 11, 4, 11, 10, 2, 10, 7, 6, 7, 1, 8, 3, 9, 4, 3, 4, 2, 3, 2, 6, 3, 6, 8, 3, 8, 9, 4, 9, 5, 2, 4, 11, 6, 2, 10, 8, 6, 7, 9, 8, 1 }; + vector3s.verts = new List(vector3); + List nums = new List(numArray); + vector3s.cache = new Dictionary(); + subdivisions = Mathf.Clamp(subdivisions, 0, 8); + for (int i = 0; i < subdivisions; i++) + { + vector3s.cache.Clear(); + List nums1 = new List(nums.Count * 4); + for (int j = 0; j < nums.Count; j += 3) + { + int item = nums[j]; + int num = nums[j + 1]; + int item1 = nums[j + 2]; + int num1 = LVUtils.u003cGenerateIcoSphereu003eg__Midpointu007c12_0(item, num, ref vector3s); + int num2 = LVUtils.u003cGenerateIcoSphereu003eg__Midpointu007c12_0(num, item1, ref vector3s); + int num3 = LVUtils.u003cGenerateIcoSphereu003eg__Midpointu007c12_0(item1, item, ref vector3s); + nums1.AddRange((IEnumerable)(new Int32[] { item, num1, num3, num, num2, num1, item1, num3, num2, num1, num2, num3 })); + } + nums = nums1; + } + for (int k = 0; k < vector3s.verts.Count; k++) + { + List vector3s1 = vector3s.verts; + Vector3 vector31 = vector3s.verts[k]; + vector3s1[k] = vector31.get_normalized() * radius; + } + Mesh mesh = new Mesh(); + mesh.set_name(String.Format("IcoSphere_{0}", subdivisions)); + Mesh mesh1 = mesh; + mesh1.SetVertices(vector3s.verts); + mesh1.SetTriangles(nums, 0); + mesh1.RecalculateNormals(); + mesh1.RecalculateBounds(); + return mesh1; + } + + public static Vector3[] GetPlaneVertices(Vector3 center, Quaternion rotation, float size) + { + Vector3 vector3 = (rotation * Vector3.get_right()) * size; + Vector3 vector31 = (rotation * Vector3.get_up()) * size; + return new Vector3[] { (center - vector3) - vector31, (center - vector3) + vector31, (center + vector3) + vector31, (center + vector3) - vector31 }; + } + + public static bool IsInPrefabAsset(object obj) + { + return false; + } + + public static void MarkDirty(object obj) + { + } + + public static float Remap(float value, float MinOld, float MaxOld, float MinNew, float MaxNew) + { + float minNew = MinNew + (value - MinOld) * (MaxNew - MinNew) / (MaxOld - MinOld); + return minNew; + } + + public static float RemapTo01(float value, float MinOld, float MaxOld) + { + return (value - MinOld) / (MaxOld - MinOld); + } + + public static void SaveAsAsset(object asset, string assetPath) + { + Debug.LogError("[LightVolumeUtils] You can only save asset in editor!"); + } + + public static void SaveAsAssetDelayed(object asset, string assetPath, Action callback = null) + { + Debug.LogError("[LightVolumeUtils] You can only save asset in editor!"); + } + + public static void SetLossyScale(Transform transform, Vector3 targetLossyScale, int maxIterations = 20) + { + Vector3 _localScale = transform.get_localScale(); + for (int i = 0; i < maxIterations; i++) + { + transform.set_localScale(_localScale); + Vector3 _lossyScale = transform.get_lossyScale(); + Vector3 vector3 = new Vector3((_lossyScale.x != 0f ? targetLossyScale.x / _lossyScale.x : 1f), (_lossyScale.y != 0f ? targetLossyScale.y / _lossyScale.y : 1f), (_lossyScale.z != 0f ? targetLossyScale.z / _lossyScale.z : 1f)); + _localScale = new Vector3(_localScale.x * vector3.x, _localScale.y * vector3.y, _localScale.z * vector3.z); + } + } + + public static void TextureSetReadWrite(Texture texture, bool enabled) + { + } + + public static Vector3 TransformPoint(Vector3 point, Vector3 position, Quaternion rotation, Vector3 scale) + { + Vector3 vector3 = (rotation * Vector3.Scale(point, scale)) + position; + return vector3; + } + } +} \ No newline at end of file diff --git a/VRCLightVolumes/LightVolume.cs b/VRCLightVolumes/LightVolume.cs new file mode 100644 index 0000000..22a5206 --- /dev/null +++ b/VRCLightVolumes/LightVolume.cs @@ -0,0 +1,290 @@ +using System; +using UnityEngine; + +namespace VRCLightVolumes +{ + [ExecuteAlways] + public class LightVolume : MonoBehaviour + { + [Header("Volume Setup")] + [Tooltip("Defines whether this volume can be moved in runtime. Disabling this option slightly improves performance.")] + public bool Dynamic; + + [Tooltip("Additive volumes apply their light on top of others as an overlay. Useful for movable and togglable lights. They can also project light onto static lightmapped objects if the surface shader supports it.")] + public bool Additive; + + [ColorUsage(false)] + [Tooltip("Multiplies the volume’s color by this value.")] + public UnityEngine.Color Color = UnityEngine.Color.get_white(); + + [Tooltip("Brightness of the volume.")] + public float Intensity = 1f; + + [Range(0f, 1f)] + [Tooltip("Size in meters of this Light Volume's overlapping regions for smooth blending with other volumes.")] + public float SmoothBlending = 0.25f; + + [Header("Baked Data")] + [Tooltip("Texture3D with baked SH data required for future atlas packing. It won't be uploaded to VRChat. (L0r, L0g, L0b, L1r.z)")] + public Texture3D Texture0; + + [Tooltip("Texture3D with baked SH data required for future atlas packing. It won't be uploaded to VRChat. (L1r.x, L1g.x, L1b.x, L1g.z)")] + public Texture3D Texture1; + + [Tooltip("Texture3D with baked SH data required for future atlas packing. It won't be uploaded to VRChat. (L1r.y, L1g.y, L1b.y, L1b.z)")] + public Texture3D Texture2; + + [Tooltip("Optional Texture3D with baked shadow mask data for future atlas packing. It won't be uploaded to VRChat. Stores occlusion for up to 4 nearby point light volumes.")] + public Texture3D ShadowsTexture; + + [Header("Color Correction")] + [Tooltip("Makes volume brighter or darker")] + public float Exposure = 0f; + + [Range(-1f, 1f)] + [Tooltip("Makes dark volume colors brighter or darker.")] + public float Shadows = 0f; + + [Range(-1f, 1f)] + [Tooltip("Makes bright volume colors brighter or darker.")] + public float Highlights = 0f; + + [Header("Baking Setup")] + [Tooltip("Uncheck it if you don't want to rebake this volume's textures.")] + public bool Bake = true; + + [Tooltip("Uncheck it if you don't want to rebake occlusion data required for baked point light volumes shadows.")] + public bool PointLightShadows = true; + + [Tooltip("Shadow Mask will use the regular volume resolution multiplied by this value.")] + public float ShadowsScale = 1f; + + [Tooltip("Post-processes the baked occlusion texture with a softening blur. This can help mitigate 'blocky' shadows caused by aliasing, but also makes shadows less crispy.")] + public bool BlurShadows = true; + + [Tooltip("Automatically sets the resolution based on the Voxels Per Unit value.")] + public bool AdaptiveResolution = true; + + [Tooltip("Number of voxels used per meter, linearly. This value increases the Light Volume file size cubically.")] + public float VoxelsPerUnit = 3f; + + [Tooltip("Manual Light Volume resolution in voxel count.")] + public Vector3Int Resolution = new Vector3Int(16, 16, 16); + + public bool PreviewVoxels; + + public VRCLightVolumes.LightVolumeInstance LightVolumeInstance; + + public VRCLightVolumes.LightVolumeSetup LightVolumeSetup; + + private Vector3[] _probesPositions = new Vector3[0]; + + private Vector3 _prevPos = Vector3.get_zero(); + + private Quaternion _prevRot = Quaternion.get_identity(); + + private Vector3 _prevScl = Vector3.get_one(); + + private float _prevExposure = 0f; + + private float _prevShadows = 0f; + + private float _prevHighlights = 0f; + + private float _lastTimeColorCorrection = 0f; + + private Material _previewMaterial; + + private Mesh _previewMesh; + + private ComputeBuffer _posBuf; + + private ComputeBuffer _argsBuf; + + private readonly static int _previewPosID; + + private readonly static int _previewScaleID; + + private bool _isValidated = false; + + public Vector3Int OcclusionResolution + { + get + { + return new Vector3Int((int)((float)this.Resolution.get_x() * this.ShadowsScale), (int)((float)this.Resolution.get_y() * this.ShadowsScale), (int)((float)this.Resolution.get_z() * this.ShadowsScale)); + } + } + + static LightVolume() + { + LightVolume._previewPosID = Shader.PropertyToID("_Positions"); + LightVolume._previewScaleID = Shader.PropertyToID("_Scale"); + } + + public LightVolume() + { + } + + public Matrix4x4 GetMatrixTRS() + { + Matrix4x4 matrix4x4 = Matrix4x4.TRS(this.GetPosition(), this.GetRotation(), this.GetScale()); + return matrix4x4; + } + + public Vector3[] GetOcclusionProbesPositions() + { + Vector3[] vector3Array = new Vector3[this.GetOcclusionVoxelCount(0)]; + Vector3 vector3 = new Vector3(0.5f, 0.5f, 0.5f); + Vector3 position = this.GetPosition(); + Quaternion rotation = this.GetRotation(); + Vector3 scale = this.GetScale(); + int num = 0; + Vector3Int occlusionResolution = this.OcclusionResolution; + for (int i = 0; i < occlusionResolution.get_z(); i++) + { + for (int j = 0; j < occlusionResolution.get_y(); j++) + { + for (int k = 0; k < occlusionResolution.get_x(); k++) + { + Vector3 vector31 = new Vector3((float)((float)k + 0.5f) / (float)occlusionResolution.get_x(), (float)((float)j + 0.5f) / (float)occlusionResolution.get_y(), (float)((float)i + 0.5f) / (float)occlusionResolution.get_z()) - vector3; + vector3Array[num] = LVUtils.TransformPoint(vector31, position, rotation, scale); + num++; + } + } + } + return vector3Array; + } + + public int GetOcclusionVoxelCount(int padding = 0) + { + Vector3Int occlusionResolution = this.OcclusionResolution; + long _x = (long)(occlusionResolution.get_x() + padding * 2); + occlusionResolution = this.OcclusionResolution; + long _y = _x * (long)(occlusionResolution.get_y() + padding * 2); + occlusionResolution = this.OcclusionResolution; + ulong _z = (ulong)(_y * (long)(occlusionResolution.get_z() + padding * 2)); + return ((_z > (long)2147483647 ? false : _z >= (long)0) ? (int)_z : -1); + } + + public Vector3 GetPosition() + { + return base.get_transform().get_position(); + } + + public Quaternion GetRotation() + { + Quaternion quaternion; + this.SetupDependencies(); + quaternion = ((!this.LightVolumeSetup.IsBakeryMode || Application.get_isPlaying() ? true : !this.Bake) ? base.get_transform().get_rotation() : Quaternion.get_identity()); + return quaternion; + } + + public Vector3 GetScale() + { + return base.get_transform().get_lossyScale(); + } + + public int GetVoxelCount(int padding = 0) + { + ulong _x = (ulong)((long)(this.Resolution.get_x() + padding * 2) * (long)(this.Resolution.get_y() + padding * 2) * (long)(this.Resolution.get_z() + padding * 2)); + return ((_x > (long)2147483647 ? false : _x >= (long)0) ? (int)_x : -1); + } + + public void Recalculate() + { + if (this.AdaptiveResolution) + { + this.RecalculateAdaptiveResolution(); + } + if ((!this.PreviewVoxels ? false : this.Bake)) + { + this.RecalculateProbesPositions(); + } + } + + public void RecalculateAdaptiveResolution() + { + Vector3 scale = this.GetScale(); + scale = new Vector3(Mathf.Abs(scale.x), Mathf.Abs(scale.y), Mathf.Abs(scale.z)); + Vector3 voxelsPerUnit = scale * this.VoxelsPerUnit; + int num = Mathf.Max((int)Mathf.Round(voxelsPerUnit.x), 1); + int num1 = Mathf.Max((int)Mathf.Round(voxelsPerUnit.y), 1); + int num2 = Mathf.Max((int)Mathf.Round(voxelsPerUnit.z), 1); + this.Resolution = new Vector3Int(num, num1, num2); + } + + public void RecalculateProbesPositions() + { + this._probesPositions = new Vector3[this.GetVoxelCount(0)]; + Vector3 vector3 = new Vector3(0.5f, 0.5f, 0.5f); + Vector3 position = this.GetPosition(); + Quaternion rotation = this.GetRotation(); + Vector3 scale = this.GetScale(); + int num = 0; + for (int i = 0; i < this.Resolution.get_z(); i++) + { + for (int j = 0; j < this.Resolution.get_y(); j++) + { + for (int k = 0; k < this.Resolution.get_x(); k++) + { + Vector3 vector31 = new Vector3((float)((float)k + 0.5f) / (float)this.Resolution.get_x(), (float)((float)j + 0.5f) / (float)this.Resolution.get_y(), (float)((float)i + 0.5f) / (float)this.Resolution.get_z()) - vector3; + this._probesPositions[num] = LVUtils.TransformPoint(vector31, position, rotation, scale); + num++; + } + } + } + } + + public void Reset() + { + ReflectionProbe reflectionProbe = null; + if ((base.get_transform().get_parent() == null ? false : base.get_transform().get_parent().get_gameObject().TryGetComponent(ref reflectionProbe))) + { + Transform _transform = base.get_transform(); + Bounds _bounds = reflectionProbe.get_bounds(); + _transform.SetPositionAndRotation(_bounds.get_center(), Quaternion.get_identity()); + Transform transform = base.get_transform(); + _bounds = reflectionProbe.get_bounds(); + LVUtils.SetLossyScale(transform, _bounds.get_size(), 20); + } + } + + public void ResetProbesPositions() + { + this._probesPositions = new Vector3[0]; + } + + public void SetupBakeryDependencies() + { + } + + public void SetupDependencies() + { + if ((this.LightVolumeInstance != null ? false : !base.TryGetComponent(ref this.LightVolumeInstance))) + { + this.LightVolumeInstance = base.get_gameObject().AddComponent(); + } + if (this.LightVolumeSetup == null) + { + this.LightVolumeSetup = UnityEngine.Object.FindObjectOfType(); + if (this.LightVolumeSetup == null) + { + this.LightVolumeSetup = (new GameObject("Light Volume Manager")).AddComponent(); + this.LightVolumeSetup.SyncUdonScript(); + } + } + } + + private void SyncUdonScript() + { + this.SetupDependencies(); + this.LightVolumeInstance.IsInitialized = true; + this.LightVolumeInstance.LightVolumeManager = this.LightVolumeSetup.LightVolumeManager; + this.LightVolumeInstance.IsDynamic = this.Dynamic; + this.LightVolumeInstance.IsAdditive = this.Additive; + this.LightVolumeInstance.Color = this.Color; + this.LightVolumeInstance.Intensity = this.Intensity; + this.LightVolumeInstance.SetSmoothBlending(this.SmoothBlending); + } + } +} \ No newline at end of file diff --git a/VRCLightVolumes/LightVolumeData.cs b/VRCLightVolumes/LightVolumeData.cs new file mode 100644 index 0000000..2bf90f6 --- /dev/null +++ b/VRCLightVolumes/LightVolumeData.cs @@ -0,0 +1,18 @@ +using System; + +namespace VRCLightVolumes +{ + [Serializable] + public struct LightVolumeData + { + public float Weight; + + public VRCLightVolumes.LightVolumeInstance LightVolumeInstance; + + public LightVolumeData(float weight, VRCLightVolumes.LightVolumeInstance lightVolumeInstance) + { + this.Weight = weight; + this.LightVolumeInstance = lightVolumeInstance; + } + } +} \ No newline at end of file diff --git a/VRCLightVolumes/LightVolumeDataSorter.cs b/VRCLightVolumes/LightVolumeDataSorter.cs new file mode 100644 index 0000000..bd15823 --- /dev/null +++ b/VRCLightVolumes/LightVolumeDataSorter.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; + +namespace VRCLightVolumes +{ + public static class LightVolumeDataSorter + { + public static LightVolumeInstance[] GetData(List sortedData) + { + int count = sortedData.Count; + LightVolumeInstance[] lightVolumeInstance = new LightVolumeInstance[count]; + for (int i = 0; i < count; i++) + { + lightVolumeInstance[i] = sortedData[i].LightVolumeInstance; + } + return lightVolumeInstance; + } + + public static List SortData(List lightVolumeDataList) + { + lightVolumeDataList.RemoveAll((LightVolumeData item) => item.LightVolumeInstance == null); + List list = ( + from item in lightVolumeDataList + orderby item.LightVolumeInstance.IsAdditive descending, item.Weight descending + select item).ToList(); + return list; + } + } +} \ No newline at end of file diff --git a/VRCLightVolumes/LightVolumeInstance.cs b/VRCLightVolumes/LightVolumeInstance.cs new file mode 100644 index 0000000..d1f53d1 --- /dev/null +++ b/VRCLightVolumes/LightVolumeInstance.cs @@ -0,0 +1,143 @@ +using System; +using UnityEngine; + +namespace VRCLightVolumes +{ + public class LightVolumeInstance : MonoBehaviour + { + [ColorUsage(false)] + [Tooltip("Changing the color is useful for animating Additive volumes. You can even control the R, G, B channels separately this way.")] + public UnityEngine.Color Color = UnityEngine.Color.get_white(); + + [Tooltip("Color multiplies by this value.")] + public float Intensity = 1f; + + [Tooltip("Defines whether this volume can be moved in runtime. Disabling this option slightly improves performance. You can even change it in runtime.")] + public bool IsDynamic = false; + + [Tooltip("Additive volumes apply their light on top of others as an overlay. Useful for movable lights like flashlights, projectors, disco balls, etc. They can also project light onto static lightmapped objects if the surface shader supports it.")] + public bool IsAdditive = false; + + [Tooltip("Inverse rotation of the pose the volume was baked in. Automatically recalculated for dynamic volumes with auto-update, or manually via the UpdateRotation() method.")] + public Quaternion InvBakedRotation = Quaternion.get_identity(); + + [Space] + [Tooltip("Min bounds of Texture0 in 3D atlas space. W stores Scale X.)")] + public Vector4 BoundsUvwMin0 = new Vector4(); + + [Tooltip("Min bounds of Texture1 in 3D atlas space. W stores Scale Y.")] + public Vector4 BoundsUvwMin1 = new Vector4(); + + [Tooltip("Min bounds of Texture2 in 3D atlas space. W stores Scale Z.")] + public Vector4 BoundsUvwMin2 = new Vector4(); + + [Tooltip("Min bounds of occlusion texture in 3D atlas space.")] + public Vector4 BoundsUvwMinOcclusion = new Vector4(); + + [Space] + [Tooltip("Max bounds of Texture0 in 3D atlas space. (Legacy)")] + public Vector4 BoundsUvwMax0 = new Vector4(); + + [Tooltip("Max bounds of Texture1 in 3D atlas space. (Legacy)")] + public Vector4 BoundsUvwMax1 = new Vector4(); + + [Tooltip("Max bounds of Texture2 in 3D atlas space. (Legacy)")] + public Vector4 BoundsUvwMax2 = new Vector4(); + + [Space] + [Tooltip("Inversed edge smoothing in 3D atlas space. Recalculates via SetSmoothBlending(float radius) method.")] + public Vector4 InvLocalEdgeSmoothing = new Vector4(); + + [Tooltip("Inversed TRS matrix of this volume that transforms it into the 1x1x1 cube. Recalculates via the UpdateRotation() method.")] + public Matrix4x4 InvWorldMatrix = Matrix4x4.get_identity(); + + [Tooltip("Current volume's rotation relative to the rotation it was baked with. Mandatory for dynamic volumes. Recalculates via the UpdateRotation() method.")] + public Vector4 RelativeRotation = new Vector4(0f, 0f, 0f, 1f); + + [Tooltip("Current volume's rotation matrix row 0 relative to the rotation it was baked with. Mandatory for dynamic volumes. Recalculates via the UpdateRotation() method. (Legacy)")] + public Vector3 RelativeRotationRow0 = Vector3.get_zero(); + + [Tooltip("Current volume's rotation matrix row 1 relative to the rotation it was baked with. Mandatory for dynamic volumes. Recalculates via the UpdateRotation() method. (Legacy)")] + public Vector3 RelativeRotationRow1 = Vector3.get_zero(); + + [Tooltip("True if there is any relative rotation. No relative rotation improves performance. Recalculated via the UpdateRotation() method.")] + public bool IsRotated = false; + + [Tooltip("True if the volume has baked occlusion.")] + public bool BakeOcclusion = false; + + [Tooltip("True if this Light Volume added to the Light Volumes array in LightVolumeManager. Should be always true for the Light Volumes placed in editor. Helps to initialize Light Volumes spawned in runtime.")] + public bool IsInitialized = false; + + [Tooltip("Reference to the Light Volume Manager. Needed for runtime initialization.")] + public VRCLightVolumes.LightVolumeManager LightVolumeManager; + + [HideInInspector] + public bool IsIterartedThrough = false; + + private UnityEngine.Color _prevColor = UnityEngine.Color.get_white(); + + private float _prevIntensity = 1f; + + public LightVolumeInstance() + { + } + + public void DelayInitialize() + { + if ((this.IsInitialized ? false : this.LightVolumeManager != null)) + { + this.LightVolumeManager.InitializeLightVolume(this); + } + } + + private void OnDisable() + { + if (this.LightVolumeManager != null) + { + this.LightVolumeManager.RequestUpdateVolumes(); + } + } + + private void OnEnable() + { + if (this.LightVolumeManager != null) + { + this.LightVolumeManager.RequestUpdateVolumes(); + } + } + + public void SetSmoothBlending(float radius) + { + this.InvLocalEdgeSmoothing = base.get_transform().get_lossyScale() / Mathf.Max(radius, 1E-05f); + } + + private void Update() + { + this.DelayInitialize(); + if ((this._prevColor != this.Color ? true : this._prevIntensity != this.Intensity)) + { + this._prevColor = this.Color; + this._prevIntensity = this.Intensity; + this.LightVolumeManager.RequestUpdateVolumes(); + } + } + + public void UpdateTransform() + { + Quaternion _rotation = base.get_transform().get_rotation(); + Matrix4x4 matrix4x4 = Matrix4x4.TRS(base.get_transform().get_position(), _rotation, base.get_transform().get_lossyScale()); + this.InvWorldMatrix = matrix4x4.get_inverse(); + Quaternion invBakedRotation = _rotation * this.InvBakedRotation; + this.IsRotated = Quaternion.Dot(invBakedRotation, Quaternion.get_identity()) < 0.999999f; + Matrix4x4 matrix4x41 = Matrix4x4.Rotate(invBakedRotation); + Vector4 row = matrix4x41.GetRow(0); + row.w = 0f; + this.RelativeRotationRow0 = row; + Vector4 vector4 = matrix4x41.GetRow(1); + vector4.w = 0f; + this.RelativeRotationRow1 = vector4; + this.RelativeRotation = new Vector4(invBakedRotation.x, invBakedRotation.y, invBakedRotation.z, invBakedRotation.w); + } + } +} \ No newline at end of file diff --git a/VRCLightVolumes/LightVolumeManager.cs b/VRCLightVolumes/LightVolumeManager.cs new file mode 100644 index 0000000..b7aecc9 --- /dev/null +++ b/VRCLightVolumes/LightVolumeManager.cs @@ -0,0 +1,538 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace VRCLightVolumes +{ + public class LightVolumeManager : MonoBehaviour + { + public const float Version = 2f; + + [Tooltip("Combined texture containing all Light Volumes' textures.")] + public Texture LightVolumeAtlas; + + [Tooltip("Combined Texture3D containing all baked Light Volume data. This field is not used at runtime, see LightVolumeAtlas instead. It specifies the base for the post process chain, if given.")] + public Texture3D LightVolumeAtlasBase; + + [Tooltip("Custom Render Textures that will be applied top to bottom to the Light Volume Atlas at runtime. External scripts can register themselves here using `RegisterPostProcessorCRT`. You probably don't want to mess with this field manually.")] + public CustomRenderTexture[] AtlasPostProcessors; + + [Tooltip("When enabled, areas outside Light Volumes fall back to light probes. Otherwise, the Light Volume with the smallest weight is used as fallback. It also improves performance.")] + public bool LightProbesBlending = true; + + [Tooltip("Disables smooth blending with areas outside Light Volumes. Use it if your entire scene's play area is covered by Light Volumes. It also improves performance.")] + public bool SharpBounds = true; + + [Tooltip("Automatically updates most of the volumes properties in runtime. Enabling/Disabling, Color and Intensity updates automatically even without this option enabled. Position, Rotation and Scale gets updated only for volumes that are marked dynamic.")] + public bool AutoUpdateVolumes = false; + + [Tooltip("Limits the maximum number of additive volumes that can affect a single pixel. If you have many dynamic additive volumes that may overlap, it's good practice to limit overdraw to maintain performance.")] + public int AdditiveMaxOverdraw = 4; + + [Tooltip("The minimum brightness at a point due to lighting from a Point Light Volume, before the light is culled. Larger values will result in better performance, but light attenuation will be less physically correct.")] + public float LightsBrightnessCutoff = 0.35f; + + [Tooltip("All Light Volume instances sorted in decreasing order by weight. You can enable or disable volumes game objects at runtime. Manually disabling unnecessary volumes improves performance.")] + public LightVolumeInstance[] LightVolumeInstances = new LightVolumeInstance[0]; + + [Tooltip("All Point Light Volume instances. You can enable or disable point light volumes game objects at runtime. Manually disabling unnecessary point light volumes improves performance.")] + public PointLightVolumeInstance[] PointLightVolumeInstances = new PointLightVolumeInstance[0]; + + [Tooltip("A texture array that can be used for as Cubemaps, LUT or Cookies")] + public Texture CustomTextures; + + [Tooltip("Cubemaps count that stored in CustomTextures. Cubemap array elements starts from the beginning, 6 elements each.")] + public int CubemapsCount = 0; + + [HideInInspector] + public bool IsRangeDirty = false; + + private bool _isInitialized = false; + + private float _prevLightsBrightnessCutoff = 0.35f; + + private Coroutine _updateCoroutine = null; + + private int _enabledCount = 0; + + private int _lastEnabledCount = -1; + + private int _additiveCount = 0; + + private int _occlusionCount = 0; + + private Vector4[] _invLocalEdgeSmooth = new Vector4[0]; + + private Vector4[] _colors = new Vector4[0]; + + private Vector4[] _boundsUvwScale = new Vector4[0]; + + private Vector4[] _boundsOcclusionUvw = new Vector4[0]; + + private Vector4[] _relativeRotationQuaternion = new Vector4[0]; + + private int _pointLightCount = 0; + + private int _lastPointLightCount = -1; + + private int[] _enabledPointIDs = new Int32[128]; + + private Vector4[] _pointLightPosition; + + private Vector4[] _pointLightColor; + + private Vector4[] _pointLightDirection; + + private Vector4[] _pointLightCustomId; + + private Matrix4x4[] _invWorldMatrix = new Matrix4x4[0]; + + private Vector4[] _boundsUvw = new Vector4[0]; + + private Vector4[] _relativeRotation = new Vector4[0]; + + private int[] _enabledIDs = new Int32[32]; + + private Vector4[] _boundsScale = new Vector4[3]; + + private Vector4[] _bounds = new Vector4[6]; + + private int lightVolumeInvLocalEdgeSmoothID; + + private int lightVolumeColorID; + + private int lightVolumeCountID; + + private int lightVolumeAdditiveCountID; + + private int lightVolumeAdditiveMaxOverdrawID; + + private int lightVolumeEnabledID; + + private int lightVolumeVersionID; + + private int lightVolumeProbesBlendID; + + private int lightVolumeSharpBoundsID; + + private int lightVolumeID; + + private int lightVolumeRotationQuaternionID; + + private int lightVolumeInvWorldMatrixID; + + private int lightVolumeUvwScaleID; + + private int lightVolumeOcclusionUvwID; + + private int lightVolumeOcclusionCountID; + + private int _pointLightPositionID; + + private int _pointLightColorID; + + private int _pointLightDirectionID; + + private int _pointLightCustomIdID; + + private int _pointLightCountID; + + private int _pointLightCubeCountID; + + private int _pointLightTextureID; + + private int _lightBrightnessCutoffID; + + private int _areaLightBrightnessCutoffID; + + private int lightVolumeRotationID; + + private int lightVolumeUvwID; + + public int EnabledCount + { + get + { + return this._enabledCount; + } + } + + public int[] EnabledIDs + { + get + { + return this._enabledIDs; + } + } + + public LightVolumeManager() + { + } + + public void InitializeLightVolume(LightVolumeInstance lightVolume) + { + int length = (int)this.LightVolumeInstances.Length; + int num = 0; + while (true) + { + if (num >= length) + { + LightVolumeInstance[] lightVolumeInstanceArray = new LightVolumeInstance[length + 1]; + Array.Copy(this.LightVolumeInstances, lightVolumeInstanceArray, length); + lightVolumeInstanceArray[length] = lightVolume; + lightVolume.IsInitialized = true; + this.LightVolumeInstances = lightVolumeInstanceArray; + break; + } + else if (this.LightVolumeInstances[num] != null) + { + num++; + } + else + { + this.LightVolumeInstances[num] = lightVolume; + lightVolume.IsInitialized = true; + break; + } + } + } + + public void InitializePointLightVolume(PointLightVolumeInstance pointLightVolume) + { + int length = (int)this.PointLightVolumeInstances.Length; + int num = 0; + while (true) + { + if (num >= length) + { + PointLightVolumeInstance[] pointLightVolumeInstanceArray = new PointLightVolumeInstance[length + 1]; + Array.Copy(this.PointLightVolumeInstances, pointLightVolumeInstanceArray, length); + pointLightVolumeInstanceArray[length] = pointLightVolume; + pointLightVolume.IsInitialized = true; + this.PointLightVolumeInstances = pointLightVolumeInstanceArray; + break; + } + else if (this.PointLightVolumeInstances[num] != null) + { + num++; + } + else + { + this.PointLightVolumeInstances[num] = pointLightVolume; + pointLightVolume.IsInitialized = true; + break; + } + } + } + + private void OnDisable() + { + this.TryInitialize(); + if ((object)this._updateCoroutine != (object)null) + { + base.StopCoroutine(this._updateCoroutine); + this._updateCoroutine = null; + } + Shader.SetGlobalFloat(this.lightVolumeEnabledID, 0f); + } + + private void OnEnable() + { + this.RequestUpdateVolumes(); + } + + public void RequestUpdateVolumes() + { + if ((this._updateCoroutine != null ? false : base.get_isActiveAndEnabled())) + { + this._updateCoroutine = base.StartCoroutine(this.UpdateVolumesCoroutine()); + } + } + + private void Start() + { + this._isInitialized = false; + this.UpdateVolumes(); + } + + private void TryInitialize() + { + if (!this._isInitialized) + { + this.lightVolumeInvLocalEdgeSmoothID = Shader.PropertyToID("_UdonLightVolumeInvLocalEdgeSmooth"); + this.lightVolumeInvWorldMatrixID = Shader.PropertyToID("_UdonLightVolumeInvWorldMatrix"); + this.lightVolumeColorID = Shader.PropertyToID("_UdonLightVolumeColor"); + this.lightVolumeCountID = Shader.PropertyToID("_UdonLightVolumeCount"); + this.lightVolumeAdditiveCountID = Shader.PropertyToID("_UdonLightVolumeAdditiveCount"); + this.lightVolumeAdditiveMaxOverdrawID = Shader.PropertyToID("_UdonLightVolumeAdditiveMaxOverdraw"); + this.lightVolumeEnabledID = Shader.PropertyToID("_UdonLightVolumeEnabled"); + this.lightVolumeVersionID = Shader.PropertyToID("_UdonLightVolumeVersion"); + this.lightVolumeProbesBlendID = Shader.PropertyToID("_UdonLightVolumeProbesBlend"); + this.lightVolumeSharpBoundsID = Shader.PropertyToID("_UdonLightVolumeSharpBounds"); + this.lightVolumeID = Shader.PropertyToID("_UdonLightVolume"); + this.lightVolumeRotationQuaternionID = Shader.PropertyToID("_UdonLightVolumeRotationQuaternion"); + this.lightVolumeUvwScaleID = Shader.PropertyToID("_UdonLightVolumeUvwScale"); + this.lightVolumeOcclusionUvwID = Shader.PropertyToID("_UdonLightVolumeOcclusionUvw"); + this.lightVolumeOcclusionCountID = Shader.PropertyToID("_UdonLightVolumeOcclusionCount"); + this._pointLightPositionID = Shader.PropertyToID("_UdonPointLightVolumePosition"); + this._pointLightColorID = Shader.PropertyToID("_UdonPointLightVolumeColor"); + this._pointLightDirectionID = Shader.PropertyToID("_UdonPointLightVolumeDirection"); + this._pointLightCountID = Shader.PropertyToID("_UdonPointLightVolumeCount"); + this._pointLightCustomIdID = Shader.PropertyToID("_UdonPointLightVolumeCustomID"); + this._pointLightCubeCountID = Shader.PropertyToID("_UdonPointLightVolumeCubeCount"); + this._pointLightTextureID = Shader.PropertyToID("_UdonPointLightVolumeTexture"); + this._lightBrightnessCutoffID = Shader.PropertyToID("_UdonLightBrightnessCutoff"); + this._areaLightBrightnessCutoffID = Shader.PropertyToID("_UdonAreaLightBrightnessCutoff"); + this.lightVolumeRotationID = Shader.PropertyToID("_UdonLightVolumeRotation"); + this.lightVolumeUvwID = Shader.PropertyToID("_UdonLightVolumeUvw"); + Shader.SetGlobalVectorArray(this.lightVolumeInvLocalEdgeSmoothID, new Vector4[32]); + Shader.SetGlobalVectorArray(this.lightVolumeColorID, new Vector4[32]); + Shader.SetGlobalMatrixArray(this.lightVolumeInvWorldMatrixID, new Matrix4x4[32]); + Shader.SetGlobalVectorArray(this.lightVolumeRotationQuaternionID, new Vector4[32]); + Shader.SetGlobalVectorArray(this.lightVolumeUvwScaleID, new Vector4[96]); + Shader.SetGlobalVectorArray(this.lightVolumeOcclusionUvwID, new Vector4[32]); + Shader.SetGlobalVectorArray(this._pointLightPositionID, new Vector4[128]); + Shader.SetGlobalVectorArray(this._pointLightColorID, new Vector4[128]); + Shader.SetGlobalVectorArray(this._pointLightDirectionID, new Vector4[128]); + Shader.SetGlobalVectorArray(this._pointLightCustomIdID, new Vector4[128]); + Shader.SetGlobalVectorArray(this.lightVolumeRotationID, new Vector4[64]); + Shader.SetGlobalVectorArray(this.lightVolumeUvwID, new Vector4[192]); + this._isInitialized = true; + } + } + + public void UpdateVolumes() + { + this.TryInitialize(); + if ((!base.get_enabled() ? false : base.get_gameObject().get_activeInHierarchy())) + { + if (this._prevLightsBrightnessCutoff != this.LightsBrightnessCutoff) + { + this._prevLightsBrightnessCutoff = this.LightsBrightnessCutoff; + this.IsRangeDirty = true; + } + this._enabledCount = 0; + this._additiveCount = 0; + this._occlusionCount = 0; + int num = 0; + while (true) + { + if ((num >= (int)this.LightVolumeInstances.Length ? true : this._enabledCount >= 32)) + { + break; + } + LightVolumeInstance lightVolumeInstances = this.LightVolumeInstances[num]; + if (lightVolumeInstances != null) + { + if ((!lightVolumeInstances.get_gameObject().get_activeInHierarchy() || lightVolumeInstances.Intensity == 0f || !(lightVolumeInstances.Color != Color.get_black()) ? true : lightVolumeInstances.IsIterartedThrough)) + { + lightVolumeInstances.IsIterartedThrough = false; + } + else + { + if (!Application.get_isPlaying()) + { + lightVolumeInstances.UpdateTransform(); + } + else if (lightVolumeInstances.IsDynamic) + { + lightVolumeInstances.UpdateTransform(); + } + if (lightVolumeInstances.IsAdditive) + { + this._additiveCount++; + } + if (lightVolumeInstances.BakeOcclusion) + { + this._occlusionCount++; + } + this._enabledIDs[this._enabledCount] = num; + this._enabledCount++; + lightVolumeInstances.IsIterartedThrough = true; + } + } + num++; + } + if (this._enabledCount != this._lastEnabledCount) + { + this._invLocalEdgeSmooth = new Vector4[this._enabledCount]; + this._relativeRotationQuaternion = new Vector4[this._enabledCount]; + this._boundsUvwScale = new Vector4[this._enabledCount * 3]; + this._boundsOcclusionUvw = new Vector4[this._enabledCount]; + this._colors = new Vector4[this._enabledCount]; + this._invWorldMatrix = new Matrix4x4[this._enabledCount]; + this._relativeRotation = new Vector4[this._enabledCount * 2]; + this._boundsUvw = new Vector4[this._enabledCount * 6]; + this._lastEnabledCount = this._enabledCount; + } + for (int i = 0; i < this._enabledCount; i++) + { + int num1 = this._enabledIDs[i]; + int num2 = i * 2; + int num3 = i * 3; + int num4 = i * 6; + LightVolumeInstance lightVolumeInstance = this.LightVolumeInstances[num1]; + lightVolumeInstance.IsIterartedThrough = false; + this._invWorldMatrix[i] = lightVolumeInstance.InvWorldMatrix; + this._invLocalEdgeSmooth[i] = lightVolumeInstance.InvLocalEdgeSmoothing; + Vector4 _linear = lightVolumeInstance.Color.get_linear() * lightVolumeInstance.Intensity; + _linear.w = (float)((lightVolumeInstance.IsRotated ? 1 : 0)); + this._colors[i] = _linear; + this._relativeRotationQuaternion[i] = lightVolumeInstance.RelativeRotation; + this._relativeRotation[num2] = lightVolumeInstance.RelativeRotationRow0; + this._relativeRotation[num2 + 1] = lightVolumeInstance.RelativeRotationRow1; + this._boundsScale[0] = lightVolumeInstance.BoundsUvwMin0; + this._boundsScale[1] = lightVolumeInstance.BoundsUvwMin1; + this._boundsScale[2] = lightVolumeInstance.BoundsUvwMin2; + this._boundsOcclusionUvw[i] = (lightVolumeInstance.BakeOcclusion ? lightVolumeInstance.BoundsUvwMinOcclusion : -Vector4.get_one()); + this._bounds[0] = lightVolumeInstance.BoundsUvwMin0; + this._bounds[1] = lightVolumeInstance.BoundsUvwMax0; + this._bounds[2] = lightVolumeInstance.BoundsUvwMin1; + this._bounds[3] = lightVolumeInstance.BoundsUvwMax1; + this._bounds[4] = lightVolumeInstance.BoundsUvwMin2; + this._bounds[5] = lightVolumeInstance.BoundsUvwMax2; + Array.Copy(this._boundsScale, 0, this._boundsUvwScale, num3, 3); + Array.Copy(this._bounds, 0, this._boundsUvw, num4, 6); + } + this._pointLightCount = 0; + int num5 = 0; + while (true) + { + if ((num5 >= (int)this.PointLightVolumeInstances.Length ? true : this._pointLightCount >= 128)) + { + break; + } + PointLightVolumeInstance pointLightVolumeInstances = this.PointLightVolumeInstances[num5]; + if (pointLightVolumeInstances != null) + { + if (this.IsRangeDirty) + { + pointLightVolumeInstances.UpdateRange(); + } + if ((!pointLightVolumeInstances.get_gameObject().get_activeInHierarchy() || pointLightVolumeInstances.Intensity == 0f || !(pointLightVolumeInstances.Color != Color.get_black()) ? true : pointLightVolumeInstances.IsIterartedThrough)) + { + pointLightVolumeInstances.IsIterartedThrough = false; + } + else + { + if (!Application.get_isPlaying()) + { + pointLightVolumeInstances.UpdateTransform(); + } + else if (pointLightVolumeInstances.IsDynamic) + { + pointLightVolumeInstances.UpdateTransform(); + } + this._enabledPointIDs[this._pointLightCount] = num5; + this._pointLightCount++; + pointLightVolumeInstances.IsIterartedThrough = true; + } + } + num5++; + } + this.IsRangeDirty = false; + if (this._pointLightCount != this._lastPointLightCount) + { + this._pointLightPosition = new Vector4[this._pointLightCount]; + this._pointLightColor = new Vector4[this._pointLightCount]; + this._pointLightDirection = new Vector4[this._pointLightCount]; + this._pointLightCustomId = new Vector4[this._pointLightCount]; + this._lastPointLightCount = this._pointLightCount; + } + for (int j = 0; j < this._pointLightCount; j++) + { + PointLightVolumeInstance pointLightVolumeInstance = this.PointLightVolumeInstances[this._enabledPointIDs[j]]; + if ((this.IsRangeDirty ? true : pointLightVolumeInstance.IsRangeDirty)) + { + pointLightVolumeInstance.UpdateRange(); + } + pointLightVolumeInstance.IsIterartedThrough = false; + Vector4 positionData = pointLightVolumeInstance.PositionData; + if (!pointLightVolumeInstance.IsAreaLight()) + { + if (!pointLightVolumeInstance.IsLut()) + { + positionData.w *= pointLightVolumeInstance.SquaredScale; + } + else + { + positionData.w /= pointLightVolumeInstance.SquaredScale; + } + } + this._pointLightPosition[j] = positionData; + Vector4 angleData = pointLightVolumeInstance.Color.get_linear() * pointLightVolumeInstance.Intensity; + angleData.w = pointLightVolumeInstance.AngleData; + this._pointLightColor[j] = angleData; + this._pointLightDirection[j] = pointLightVolumeInstance.DirectionData; + this._pointLightCustomId[j].x = pointLightVolumeInstance.CustomID; + this._pointLightCustomId[j].y = (float)pointLightVolumeInstance.ShadowmaskIndex; + this._pointLightCustomId[j].z = pointLightVolumeInstance.SquaredRange; + } + bool lightVolumeAtlas = this.LightVolumeAtlas != null; + Shader.SetGlobalFloat(this.lightVolumeVersionID, 2f); + if ((!lightVolumeAtlas || this._enabledCount == 0 ? this._pointLightCount != 0 : true)) + { + if (lightVolumeAtlas) + { + Shader.SetGlobalTexture(this.lightVolumeID, this.LightVolumeAtlas); + } + Shader.SetGlobalFloat(this.lightVolumeCountID, (float)this._enabledCount); + Shader.SetGlobalFloat(this.lightVolumeAdditiveCountID, (float)this._additiveCount); + Shader.SetGlobalFloat(this.lightVolumeOcclusionCountID, (float)this._occlusionCount); + Shader.SetGlobalFloat(this.lightVolumeProbesBlendID, (float)((this.LightProbesBlending ? 1 : 0))); + Shader.SetGlobalFloat(this.lightVolumeSharpBoundsID, (float)((this.SharpBounds ? 1 : 0))); + Shader.SetGlobalFloat(this.lightVolumeAdditiveMaxOverdrawID, (float)this.AdditiveMaxOverdraw); + if (this._enabledCount != 0) + { + Shader.SetGlobalVectorArray(this.lightVolumeInvLocalEdgeSmoothID, this._invLocalEdgeSmooth); + Shader.SetGlobalVectorArray(this.lightVolumeUvwScaleID, this._boundsUvwScale); + Shader.SetGlobalVectorArray(this.lightVolumeOcclusionUvwID, this._boundsOcclusionUvw); + Shader.SetGlobalMatrixArray(this.lightVolumeInvWorldMatrixID, this._invWorldMatrix); + Shader.SetGlobalVectorArray(this.lightVolumeRotationQuaternionID, this._relativeRotationQuaternion); + Shader.SetGlobalVectorArray(this.lightVolumeColorID, this._colors); + Shader.SetGlobalVectorArray(this.lightVolumeUvwID, this._boundsUvw); + Shader.SetGlobalVectorArray(this.lightVolumeRotationID, this._relativeRotation); + } + Shader.SetGlobalFloat(this._pointLightCountID, (float)this._pointLightCount); + Shader.SetGlobalFloat(this._pointLightCubeCountID, (float)this.CubemapsCount); + if (this._pointLightCount != 0) + { + Shader.SetGlobalVectorArray(this._pointLightColorID, this._pointLightColor); + Shader.SetGlobalVectorArray(this._pointLightPositionID, this._pointLightPosition); + Shader.SetGlobalVectorArray(this._pointLightDirectionID, this._pointLightDirection); + Shader.SetGlobalVectorArray(this._pointLightCustomIdID, this._pointLightCustomId); + Shader.SetGlobalFloat(this._lightBrightnessCutoffID, this.LightsBrightnessCutoff); + Shader.SetGlobalFloat(this._areaLightBrightnessCutoffID, this.LightsBrightnessCutoff); + } + if (this.CustomTextures != null) + { + Shader.SetGlobalTexture(this._pointLightTextureID, this.CustomTextures); + } + Shader.SetGlobalFloat(this.lightVolumeEnabledID, 1f); + } + else + { + Shader.SetGlobalFloat(this.lightVolumeEnabledID, 0f); + } + } + else + { + Shader.SetGlobalFloat(this.lightVolumeEnabledID, 0f); + } + } + + private IEnumerator UpdateVolumesCoroutine() + { + do + { + yield return null; + this.UpdateVolumes(); + } + while (this.AutoUpdateVolumes); + this._updateCoroutine = null; + } + } +} \ No newline at end of file diff --git a/VRCLightVolumes/LightVolumeSetup.cs b/VRCLightVolumes/LightVolumeSetup.cs new file mode 100644 index 0000000..3d218d3 --- /dev/null +++ b/VRCLightVolumes/LightVolumeSetup.cs @@ -0,0 +1,335 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace VRCLightVolumes +{ + [ExecuteAlways] + public class LightVolumeSetup : MonoBehaviour + { + [SerializeField] + public List LightVolumes = new List(); + + [SerializeField] + public List LightVolumesWeights = new List(); + + [SerializeField] + public List PointLightVolumes = new List(); + + [Header("Point Light Volumes")] + public LightVolumeSetup.TextureArrayResolution Resolution = LightVolumeSetup.TextureArrayResolution._128x128; + + public LightVolumeSetup.TextureArrayFormat Format = LightVolumeSetup.TextureArrayFormat.RGBAHalf; + + [Range(0.05f, 1f)] + [Tooltip("The minimum brightness at a point due to lighting from a Point Light Volume, before the light is culled. Larger values will result in better performance, but light attenuation will be less physically correct.")] + public float LightsBrightnessCutoff = 0.35f; + + [Header("Baking")] + [Tooltip("Bakery usually gives better results and works faster.")] + public LightVolumeSetup.Baking BakingMode = LightVolumeSetup.Baking.Progressive; + + [Tooltip("Removes baked noise in Light Volumes but may slightly reduce sharpness. Recommended to keep it enabled.")] + public bool Denoise = true; + + [Tooltip("Whether to dilate valid probe data into invalid probes, such as probes that are inside geometry. Helps mitigate light leaking.")] + public bool DilateInvalidProbes = true; + + [Range(1f, 8f)] + [Tooltip("How many iterations to run dilation for. Higher values will result in less leaking, but will also cause longer bakes.")] + public int DilationIterations = 1; + + [Range(0f, 1f)] + [Tooltip("The percentage of rays shot from a probe that should hit backfaces before the probe is considered invalid for the purpose of dilation. 0 means every probe is invalid, 1 means every probe is valid.")] + public float DilationBackfaceBias = 0.1f; + + [Tooltip("Automatically fixes Bakery's \"burned\" light probes after a scene bake. But decreases their contrast slightly.")] + public bool FixLightProbesL1 = true; + + [Header("Visuals")] + [Tooltip("When enabled, areas outside Light Volumes fall back to light probes. Otherwise, the Light Volume with the smallest weight is used as fallback. It also improves performance.")] + public bool LightProbesBlending = true; + + [Tooltip("Disables smooth blending with areas outside Light Volumes. Use it if your entire scene's play area is covered by Light Volumes. It also improves performance.")] + public bool SharpBounds = true; + + [Tooltip("Automatically updates most of the volumes properties in runtime. Enabling/Disabling, Color and Intensity updates automatically even without this option enabled. Position, Rotation and Scale gets updated only for volumes that are marked dynamic.")] + public bool AutoUpdateVolumes = false; + + [Min(1f)] + [Tooltip("Limits the maximum number of additive volumes and point light volumes that can affect a single pixel. If you have many dynamic additive or point light volumes that may overlap, it's good practice to limit overdraw to maintain performance.")] + public int AdditiveMaxOverdraw = 4; + + [Header("Debug")] + [Tooltip("Removes all Light Volume scripts in play mode, except Udon components. Useful for testing in a clean setup, just like in VRChat. For example, Auto Update Volumes and Dynamic Light Volumes will work just like in VRChat.")] + public bool DestroyInPlayMode = false; + + [SerializeField] + public List LightVolumeDataList = new List(); + + public VRCLightVolumes.LightVolumeManager LightVolumeManager; + + private bool _dontSync = true; + + public LightVolumeSetup.Baking _bakingModePrev; + + public bool IsLegacyUVWConverted = false; + + private LightVolumeSetup.TextureArrayResolution _resolutionPrev = LightVolumeSetup.TextureArrayResolution._128x128; + + private LightVolumeSetup.TextureArrayFormat _formatPrev = LightVolumeSetup.TextureArrayFormat.RGBAHalf; + + public bool DontSync + { + get + { + return (Application.get_isPlaying() ? this._dontSync : false); + } + set + { + this._dontSync = value; + } + } + + public bool IsBakeryMode + { + get + { + return this.BakingMode == LightVolumeSetup.Baking.Bakery; + } + } + + public LightVolumeSetup() + { + } + + public void BakeOcclusionVolumes() + { + } + + [RuntimeInitializeOnLoadMethod(,)] // JustDecompile was unable to locate the assembly where attribute parameters types are defined. Generating parameters values is impossible. + private static void CommitSudoku() + { + if (Application.get_isPlaying()) + { + bool flag = false; + LightVolumeSetup[] lightVolumeSetupArray = UnityEngine.Object.FindObjectsByType(1, 0); + for (int i = 0; i < (int)lightVolumeSetupArray.Length; i++) + { + if (lightVolumeSetupArray[i].DestroyInPlayMode) + { + flag = true; + } + else + { + lightVolumeSetupArray[i].DontSync = false; + } + } + if (flag) + { + LightVolume[] lightVolumeArray = UnityEngine.Object.FindObjectsByType(1, 0); + for (int j = 0; j < (int)lightVolumeArray.Length; j++) + { + UnityEngine.Object.Destroy(lightVolumeArray[j]); + } + PointLightVolume[] pointLightVolumeArray = UnityEngine.Object.FindObjectsByType(1, 0); + for (int k = 0; k < (int)pointLightVolumeArray.Length; k++) + { + UnityEngine.Object.Destroy(pointLightVolumeArray[k]); + } + for (int l = 0; l < (int)lightVolumeSetupArray.Length; l++) + { + UnityEngine.Object.Destroy(lightVolumeSetupArray[l]); + } + } + } + } + + private PointLightVolumeInstance[] GetPointLightVolumeInstances() + { + List pointLightVolumeInstances = new List(); + int count = this.PointLightVolumes.Count; + for (int i = 0; i < count; i++) + { + if ((this.PointLightVolumes[i] == null ? false : this.PointLightVolumes[i].PointLightVolumeInstance != null)) + { + pointLightVolumeInstances.Add(this.PointLightVolumes[i].PointLightVolumeInstance); + } + } + return pointLightVolumeInstances.ToArray(); + } + + public void RefreshVolumesList() + { + if (!this.DontSync) + { + LightVolume[] lightVolumeArray = UnityEngine.Object.FindObjectsOfType(true); + for (int i = 0; i < (int)lightVolumeArray.Length; i++) + { + if (!lightVolumeArray[i].CompareTag("EditorOnly")) + { + if (!this.LightVolumes.Contains(lightVolumeArray[i])) + { + this.LightVolumes.Add(lightVolumeArray[i]); + this.LightVolumesWeights.Add(0f); + } + } + } + for (int j = 0; j < this.LightVolumes.Count; j++) + { + if ((this.LightVolumes[j] == null ? true : this.LightVolumes[j].CompareTag("EditorOnly"))) + { + this.LightVolumes.RemoveAt(j); + this.LightVolumesWeights.RemoveAt(j); + j--; + } + } + PointLightVolume[] pointLightVolumeArray = UnityEngine.Object.FindObjectsOfType(true); + for (int k = 0; k < (int)pointLightVolumeArray.Length; k++) + { + if (!pointLightVolumeArray[k].CompareTag("EditorOnly")) + { + if (!this.PointLightVolumes.Contains(pointLightVolumeArray[k])) + { + this.PointLightVolumes.Add(pointLightVolumeArray[k]); + } + } + } + for (int l = 0; l < this.PointLightVolumes.Count; l++) + { + if ((this.PointLightVolumes[l] == null ? true : this.PointLightVolumes[l].CompareTag("EditorOnly"))) + { + this.PointLightVolumes.RemoveAt(l); + l--; + } + } + this.SyncUdonScript(); + } + } + + public void RegisterPostProcessorCRT(CustomRenderTexture crt) + { + if ((crt == null ? false : Array.IndexOf(this.LightVolumeManager.AtlasPostProcessors, crt) == -1)) + { + VRCLightVolumes.LightVolumeManager lightVolumeManager = this.LightVolumeManager; + if (lightVolumeManager.AtlasPostProcessors == null) + { + lightVolumeManager.AtlasPostProcessors = new CustomRenderTexture[0]; + } + Array.Resize(ref this.LightVolumeManager.AtlasPostProcessors, (int)this.LightVolumeManager.AtlasPostProcessors.Length + 1); + CustomRenderTexture[] atlasPostProcessors = this.LightVolumeManager.AtlasPostProcessors; + atlasPostProcessors[(int)atlasPostProcessors.Length - 1] = crt; + Debug.Log(String.Concat("[LightVolumeSetup] Registered post processor CRT: ", crt.get_name())); + this.UpdatePostProcessors(); + } + } + + public void SyncUdonScript() + { + if ((this.LightVolumeManager == null ? false : !this.DontSync)) + { + this.LightVolumeManager.AutoUpdateVolumes = this.AutoUpdateVolumes; + this.LightVolumeManager.LightProbesBlending = this.LightProbesBlending; + this.LightVolumeManager.SharpBounds = this.SharpBounds; + this.LightVolumeManager.AdditiveMaxOverdraw = this.AdditiveMaxOverdraw; + this.LightVolumeManager.LightsBrightnessCutoff = this.LightsBrightnessCutoff; + if (this.LightVolumes.Count != 0) + { + this.LightVolumeManager.LightVolumeInstances = LightVolumeDataSorter.GetData(LightVolumeDataSorter.SortData(this.LightVolumeDataList)); + } + if (this.PointLightVolumes.Count != 0) + { + this.LightVolumeManager.PointLightVolumeInstances = this.GetPointLightVolumeInstances(); + } + this.LightVolumeManager.UpdateVolumes(); + } + } + + public void UnregisterPostProcessorCRT(CustomRenderTexture crt) + { + if (crt != null) + { + int num = Array.IndexOf(this.LightVolumeManager.AtlasPostProcessors, crt); + if (num >= 0) + { + CustomRenderTexture[] atlasPostProcessors = new CustomRenderTexture[(int)this.LightVolumeManager.AtlasPostProcessors.Length - 1]; + int num1 = 0; + int num2 = 0; + while (num1 < (int)this.LightVolumeManager.AtlasPostProcessors.Length) + { + if (num1 != num) + { + int num3 = num2; + num2 = num3 + 1; + atlasPostProcessors[num3] = this.LightVolumeManager.AtlasPostProcessors[num1]; + } + num1++; + } + this.LightVolumeManager.AtlasPostProcessors = atlasPostProcessors; + Debug.Log(String.Concat("[LightVolumeSetup] Unregistered post processor CRT: ", crt.get_name())); + this.UpdatePostProcessors(); + } + } + } + + private void UpdatePostProcessors() + { + if ((this.LightVolumeManager.AtlasPostProcessors == null ? false : this.LightVolumeManager.AtlasPostProcessors.Length != 0)) + { + Texture3D lightVolumeAtlasBase = this.LightVolumeManager.LightVolumeAtlasBase; + Texture texture = lightVolumeAtlasBase; + CustomRenderTexture[] atlasPostProcessors = this.LightVolumeManager.AtlasPostProcessors; + for (int i = 0; i < (int)atlasPostProcessors.Length; i++) + { + CustomRenderTexture customRenderTexture = atlasPostProcessors[i]; + if (customRenderTexture != null) + { + customRenderTexture.Release(); + customRenderTexture.set_dimension(3); + customRenderTexture.set_graphicsFormat(48); + customRenderTexture.set_updateMode(1); + if (lightVolumeAtlasBase != null) + { + customRenderTexture.set_width(lightVolumeAtlasBase.get_width()); + customRenderTexture.set_height(lightVolumeAtlasBase.get_height()); + customRenderTexture.set_volumeDepth(lightVolumeAtlasBase.get_depth()); + } + customRenderTexture.get_material().set_mainTexture(texture); + texture = customRenderTexture; + this.LightVolumeManager.LightVolumeAtlas = customRenderTexture; + customRenderTexture.Update(); + } + } + } + else + { + this.LightVolumeManager.LightVolumeAtlas = this.LightVolumeManager.LightVolumeAtlasBase; + } + } + + public enum Baking + { + Progressive, + Bakery + } + + public enum TextureArrayFormat + { + RGBA32 = 4, + RGBAHalf = 17, + RGBAFloat = 20 + } + + public enum TextureArrayResolution + { + _16x16 = 16, + _32x32 = 32, + _64x64 = 64, + _128x128 = 128, + _256x256 = 256, + _512x512 = 512, + _1024x1024 = 1024, + _2048x2048 = 2048 + } + } +} \ No newline at end of file diff --git a/VRCLightVolumes/PointLightVolume.cs b/VRCLightVolumes/PointLightVolume.cs new file mode 100644 index 0000000..29b51ef --- /dev/null +++ b/VRCLightVolumes/PointLightVolume.cs @@ -0,0 +1,268 @@ +using System; +using UnityEngine; + +namespace VRCLightVolumes +{ + [ExecuteAlways] + public class PointLightVolume : MonoBehaviour + { + [Tooltip("Defines whether this point light volume can be moved in runtime. Disabling this option slightly improves performance.")] + public bool Dynamic = false; + + [Tooltip("Enables baked shadows for this light. This setting is only available for static lights, which cannot move. You must re-bake your volumes after changing this setting. This incurs some runtime VRAM and performance overhead.")] + public bool BakedShadows = false; + + [Min(0f)] + [Tooltip("Shadow radius for the baked shadows. Higher values will produce softer shadows.")] + public float BakedShadowRadius = 0.1f; + + [Tooltip("Point light is the most performant type. Area light is the heaviest and best suited for dynamic, movable sources. For static lighting, it's recommended to bake regular additive light volumes instead.")] + public PointLightVolume.LightType Type = PointLightVolume.LightType.PointLight; + + [Min(0.0001f)] + [Tooltip("Physical radius of a light source if it was a matte glowing sphere for a point light, or a flashlight reflector for a spot light. Larger size emmits more light without increasing overall intensity.")] + public float LightSourceSize = 0.25f; + + [Min(0.0001f)] + [Tooltip("Radius in meters beyond which light is culled. Fewer overlapping lights result in better performance.")] + public float Range = 10f; + + [ColorUsage(false)] + [Tooltip("Multiplies the point light volume’s color by this value.")] + public UnityEngine.Color Color = UnityEngine.Color.get_white(); + + [Tooltip("Brightness of the point light volume.")] + public float Intensity = 1f; + + [Tooltip("Parametric uses settings to compute light falloff. LUT uses a texture: X - cone falloff, Y - attenuation (Y only for point lights). Cookie projects a texture for spot lights. Cubemap projects a cubemap for point lights.")] + public PointLightVolume.LightShape Shape = PointLightVolume.LightShape.Parametric; + + [Range(0.1f, 360f)] + [Tooltip("Angle of a spotlight cone in degrees.")] + public float Angle = 60f; + + [Range(0.001f, 1f)] + [Tooltip("Cone falloff.")] + public float Falloff = 1f; + + [Tooltip("X - cone falloff, Y - attenuation. No compression and RGBA Float or RGBA Half format is recommended.")] + public Texture2D FalloffLUT = null; + + [Tooltip("Projects a square texture for spot lights.")] + public Texture2D Cookie = null; + + [Tooltip("Projects a cubemap for point lights.")] + public UnityEngine.Cubemap Cubemap = null; + + [Tooltip("Shows overdrawing range gizmo. Less point light volumes intersections - more performance!")] + public bool DebugRange = false; + + public int CustomID = 0; + + public VRCLightVolumes.PointLightVolumeInstance PointLightVolumeInstance; + + public VRCLightVolumes.LightVolumeSetup LightVolumeSetup; + + private Texture2D _falloffLUTPrev = null; + + private Texture2D _cookiePrev = null; + + private UnityEngine.Cubemap _cubemapPrev = null; + + private PointLightVolume.LightShape _shapePrev = PointLightVolume.LightShape.Parametric; + + private PointLightVolume.LightType _typePrev = PointLightVolume.LightType.PointLight; + + private Vector3 _prevPos = Vector3.get_zero(); + + private Quaternion _prevRot = Quaternion.get_identity(); + + private Vector3 _prevScl = Vector3.get_one(); + + private bool _isValidated = false; + + public PointLightVolume() + { + } + + public Texture GetCustomTexture() + { + Texture falloffLUT; + if ((this.Shape == PointLightVolume.LightShape.Parametric ? false : this.Type != PointLightVolume.LightType.AreaLight)) + { + if (this.Type != PointLightVolume.LightType.PointLight) + { + if (this.Type == PointLightVolume.LightType.SpotLight) + { + if (this.Shape == PointLightVolume.LightShape.LUT) + { + falloffLUT = this.FalloffLUT; + return falloffLUT; + } + else if (this.Shape == PointLightVolume.LightShape.Custom) + { + falloffLUT = this.Cookie; + return falloffLUT; + } + } + } + else if (this.Shape == PointLightVolume.LightShape.LUT) + { + falloffLUT = this.FalloffLUT; + return falloffLUT; + } + else if (this.Shape == PointLightVolume.LightShape.Custom) + { + falloffLUT = this.Cubemap; + return falloffLUT; + } + falloffLUT = null; + } + else + { + falloffLUT = null; + } + return falloffLUT; + } + + private void OnDestroy() + { + if (this.LightVolumeSetup != null) + { + this.FalloffLUT = null; + this.Cookie = null; + this.Cubemap = null; + this.LightVolumeSetup.RefreshVolumesList(); + this.LightVolumeSetup.SyncUdonScript(); + } + } + + private void OnDisable() + { + if (this.LightVolumeSetup != null) + { + this.LightVolumeSetup.RefreshVolumesList(); + this.LightVolumeSetup.SyncUdonScript(); + } + } + + private void OnEnable() + { + this.SetupDependencies(); + this.LightVolumeSetup.RefreshVolumesList(); + this.LightVolumeSetup.SyncUdonScript(); + } + + private void OnValidate() + { + this._isValidated = true; + } + + private void Reset() + { + this.SetupDependencies(); + this.SyncUdonScript(); + this.LightVolumeSetup.RefreshVolumesList(); + this.LightVolumeSetup.SyncUdonScript(); + } + + public void SetupDependencies() + { + if ((this.PointLightVolumeInstance != null ? false : !base.TryGetComponent(ref this.PointLightVolumeInstance))) + { + this.PointLightVolumeInstance = base.get_gameObject().AddComponent(); + } + if (this.LightVolumeSetup == null) + { + this.LightVolumeSetup = UnityEngine.Object.FindObjectOfType(); + if (this.LightVolumeSetup == null) + { + this.LightVolumeSetup = (new GameObject("Light Volume Manager")).AddComponent(); + this.LightVolumeSetup.SyncUdonScript(); + } + } + } + + public void SyncUdonScript() + { + if (base.get_gameObject() != null) + { + this.SetupDependencies(); + this.PointLightVolumeInstance.IsInitialized = true; + this.PointLightVolumeInstance.LightVolumeManager = this.LightVolumeSetup.LightVolumeManager; + this.PointLightVolumeInstance.IsDynamic = this.Dynamic; + this.PointLightVolumeInstance.Color = this.Color; + this.PointLightVolumeInstance.Intensity = this.Intensity; + this.PointLightVolumeInstance.IsRangeDirty = true; + if (this.Type == PointLightVolume.LightType.PointLight) + { + if ((this.Shape != PointLightVolume.LightShape.Custom ? false : this.Cubemap != null)) + { + this.PointLightVolumeInstance.SetLightSourceSize(this.LightSourceSize); + this.PointLightVolumeInstance.SetCustomTexture(this.CustomID); + } + else if ((this.Shape != PointLightVolume.LightShape.LUT ? true : this.FalloffLUT == null)) + { + this.PointLightVolumeInstance.SetLightSourceSize(this.LightSourceSize); + this.PointLightVolumeInstance.SetParametric(); + } + else + { + this.PointLightVolumeInstance.SetLightSourceSize(this.Range); + this.PointLightVolumeInstance.SetLut(this.CustomID); + } + this.PointLightVolumeInstance.SetPointLight(); + this.PointLightVolumeInstance.UpdateRotation(); + } + else if (this.Type == PointLightVolume.LightType.SpotLight) + { + this.PointLightVolumeInstance.SetLightSourceSize(this.Range); + if ((this.Shape != PointLightVolume.LightShape.Custom ? false : this.Cookie != null)) + { + this.PointLightVolumeInstance.SetLightSourceSize(this.LightSourceSize); + this.PointLightVolumeInstance.SetCustomTexture(this.CustomID); + } + else if ((this.Shape != PointLightVolume.LightShape.LUT ? true : this.FalloffLUT == null)) + { + this.PointLightVolumeInstance.SetLightSourceSize(this.LightSourceSize); + this.PointLightVolumeInstance.SetParametric(); + } + else + { + this.PointLightVolumeInstance.SetLightSourceSize(this.Range); + this.PointLightVolumeInstance.SetLut(this.CustomID); + } + this.PointLightVolumeInstance.SetSpotLight(this.Angle, this.Falloff); + this.PointLightVolumeInstance.UpdateRotation(); + } + else if (this.Type == PointLightVolume.LightType.AreaLight) + { + this.PointLightVolumeInstance.SetAreaLight(); + this.PointLightVolumeInstance.UpdateRotation(); + } + } + } + + private void Update() + { + if (base.get_gameObject() != null) + { + this.SetupDependencies(); + } + } + + public enum LightShape + { + Parametric, + LUT, + Custom + } + + public enum LightType + { + PointLight, + SpotLight, + AreaLight + } + } +} \ No newline at end of file diff --git a/VRCLightVolumes/PointLightVolumeInstance.cs b/VRCLightVolumes/PointLightVolumeInstance.cs new file mode 100644 index 0000000..5b4e19b --- /dev/null +++ b/VRCLightVolumes/PointLightVolumeInstance.cs @@ -0,0 +1,313 @@ +using System; +using UnityEngine; + +namespace VRCLightVolumes +{ + public class PointLightVolumeInstance : MonoBehaviour + { + [ColorUsage(false)] + [Tooltip("Point light volume color")] + public UnityEngine.Color Color; + + [Tooltip("Color multiplies by this value.")] + public float Intensity = 1f; + + [Tooltip("Defines whether this point light volume can be moved in runtime. Disabling this option slightly improves performance.")] + public bool IsDynamic = false; + + [Tooltip("For point light: XYZ = Position, W = Inverse squared range.\nFor spot light: XYZ = Position, W = Inverse squared range, negated.\nFor area light: XYZ = Position, W = Width.")] + public Vector4 PositionData; + + [Tooltip("For point light: XYZW = Rotation quaternion.\nFor spot light: XYZ = Direction, W = Cone falloff.\nFor area light: XYZW = Rotation quaternion.")] + public Vector4 DirectionData; + + [Tooltip("If parametric: Stores 0.\nIf uses custom LUT: Stores LUT ID with positive sign.\nIf uses custom texture: Stores texture ID with negative sign.")] + public float CustomID; + + [Tooltip("Half-angle of the spotlight cone, in radians.")] + public float Angle; + + [Tooltip("For point light: unused.\nFor spot light: Cos of outer angle if no custom texture, tan of outer angle otherwise.\nFor area light: 2 + Height.")] + public float AngleData; + + [Tooltip("Index of the shadowmask channel used by this light. -1 means no shadowmask.")] + public sbyte ShadowmaskIndex = -1; + + [Tooltip("True if this Point Light Volume added to the Point Light Volumes array in LightVolumeManager. Should be always true for the Point Light Volumes placed in editor. Helps to initialize Point Light Volumes spawned in runtime.")] + public bool IsInitialized = false; + + [Tooltip("Squared range after which light will be culled. Should be recalculated by executing UpdateRange() method.")] + public float SquaredRange = 1f; + + [Tooltip("Average squared lossy scale of the light. Light Source Size gets multiplied by it at the end. Updates with UpdateTransform() method.")] + public float SquaredScale = 1f; + + [Tooltip("Reference to the Light Volume Manager. Needed for runtime initialization.")] + public VRCLightVolumes.LightVolumeManager LightVolumeManager; + + [HideInInspector] + public bool IsIterartedThrough = false; + + [HideInInspector] + public bool IsRangeDirty = false; + + private Vector3 _prevPosition = Vector3.get_zero(); + + private Quaternion _prevRotation = Quaternion.get_identity(); + + private Vector3 _prevScale = Vector3.get_one(); + + public PointLightVolumeInstance() + { + } + + private float ComputeAreaLightSquaredBoundingSphere(float width, float height, UnityEngine.Color color, float intensity, float cutoff) + { + float single = Mathf.Clamp(cutoff / (Mathf.Max(color.r, Mathf.Max(color.g, color.b)) * intensity), -6.2831855f, 6.2831855f); + float single1 = width * height; + float single2 = width * width; + float single3 = 0.25f * (single2 + height * height); + float single4 = Mathf.Tan(0.25f * single); + float single5 = single4 * single4; + float single6 = single5 * single3; + float single7 = Mathf.Sqrt(single6 * single6 + 4f * single5 * single1 * single1); + return (single7 - single6) * 0.125f / single5; + } + + private float ComputePointLightSquaredBoundingSphere(UnityEngine.Color color, float intensity, float sqSize, float cutoff) + { + float single = Mathf.Max(color.r, Mathf.Max(color.g, color.b)); + float single1 = Mathf.Max(6.2831855f * single * Mathf.Abs(intensity) / (cutoff * cutoff) - 1f, 0f) * sqSize; + return single1; + } + + public bool IsAreaLight() + { + return (this.PositionData.w < 0f ? false : (double)this.AngleData > 1.5); + } + + public bool IsCustomTexture() + { + return this.CustomID < 0f; + } + + public bool IsLut() + { + return this.CustomID > 0f; + } + + public bool IsParametric() + { + return this.CustomID == 0f; + } + + public bool IsPointLight() + { + return (this.PositionData.w < 0f ? false : (double)this.AngleData <= 1.5); + } + + public bool IsSpotLight() + { + return this.PositionData.w < 0f; + } + + private void MarkRangeDirtyAndRequestUpdate() + { + this.IsRangeDirty = true; + } + + private void OnDisable() + { + if (this.LightVolumeManager != null) + { + this.LightVolumeManager.RequestUpdateVolumes(); + } + } + + private void OnEnable() + { + if (this.LightVolumeManager != null) + { + this.LightVolumeManager.RequestUpdateVolumes(); + } + } + + public void SetAreaLight() + { + this.PositionData.w = Mathf.Max(Mathf.Abs(base.get_transform().get_lossyScale().x), 0.001f); + this.AngleData = 2f + Mathf.Max(Mathf.Abs(base.get_transform().get_lossyScale().y), 0.001f); + this.MarkRangeDirtyAndRequestUpdate(); + } + + public void SetColor(UnityEngine.Color color) + { + this.Color = color; + this.MarkRangeDirtyAndRequestUpdate(); + } + + public void SetCustomTexture(int id) + { + this.CustomID = (float)(-id - 1); + if (this.IsSpotLight()) + { + this.AngleData = Mathf.Tan(this.Angle); + } + this.MarkRangeDirtyAndRequestUpdate(); + } + + public void SetIntensity(float intensity) + { + this.Intensity = intensity; + this.MarkRangeDirtyAndRequestUpdate(); + } + + public void SetLightSourceSize(float size) + { + if (!this.IsLut()) + { + this.PositionData.w = Mathf.Sign(this.PositionData.w) * size * size; + } + else + { + this.PositionData.w = Mathf.Sign(this.PositionData.w) / (size * size); + } + this.MarkRangeDirtyAndRequestUpdate(); + } + + public void SetLut(int id) + { + this.CustomID = (float)(id + 1); + this.AngleData = Mathf.Cos(this.Angle); + this.MarkRangeDirtyAndRequestUpdate(); + } + + public void SetParametric() + { + this.CustomID = 0f; + this.AngleData = Mathf.Cos(this.Angle); + this.MarkRangeDirtyAndRequestUpdate(); + } + + public void SetPointLight() + { + this.PositionData.w = Mathf.Abs(this.PositionData.w); + this.MarkRangeDirtyAndRequestUpdate(); + } + + public void SetSpotLight(float angleDeg, float falloff) + { + this.Angle = angleDeg * 0.017453292f * 0.5f; + if (!this.IsCustomTexture()) + { + this.AngleData = Mathf.Cos(this.Angle); + this.DirectionData.w = 1f / (Mathf.Cos(this.Angle * (1f - Mathf.Clamp01(falloff))) - this.AngleData); + } + else + { + this.AngleData = Mathf.Tan(this.Angle); + } + this.PositionData.w = -Mathf.Abs(this.PositionData.w); + this.MarkRangeDirtyAndRequestUpdate(); + } + + public void SetSpotLight(float angleDeg) + { + this.Angle = angleDeg * 0.017453292f * 0.5f; + if (!this.IsCustomTexture()) + { + this.AngleData = Mathf.Cos(this.Angle); + } + else + { + this.AngleData = Mathf.Tan(this.Angle); + } + this.PositionData.w = -Mathf.Abs(this.PositionData.w); + this.MarkRangeDirtyAndRequestUpdate(); + } + + private void Start() + { + if ((this.IsInitialized ? false : this.LightVolumeManager != null)) + { + this.LightVolumeManager.InitializePointLightVolume(this); + } + } + + public void UpdatePosition() + { + Vector3 _position = base.get_transform().get_position(); + this.PositionData = new Vector4(_position.x, _position.y, _position.z, this.PositionData.w); + } + + public void UpdateRange() + { + float single = (this.LightVolumeManager != null ? this.LightVolumeManager.LightsBrightnessCutoff : 0.35f); + if (this.IsAreaLight()) + { + this.SquaredRange = this.ComputeAreaLightSquaredBoundingSphere(Mathf.Abs(this.SquaredScale / this.PositionData.w), this.AngleData - 2f, this.Color, this.Intensity * 3.1415927f, single); + } + else if (!this.IsLut()) + { + this.SquaredRange = this.ComputePointLightSquaredBoundingSphere(this.Color, this.Intensity, Mathf.Abs(this.SquaredScale * this.PositionData.w), single); + } + else + { + this.SquaredRange = Mathf.Abs(this.SquaredScale / this.PositionData.w); + } + this.IsRangeDirty = false; + } + + public void UpdateRotation() + { + Quaternion _rotation = base.get_transform().get_rotation(); + if (this.IsAreaLight()) + { + this.DirectionData = new Vector4(_rotation.x, _rotation.y, _rotation.z, _rotation.w); + } + else if ((!this.IsSpotLight() ? false : !this.IsCustomTexture())) + { + Vector3 _forward = base.get_transform().get_forward(); + this.DirectionData = new Vector4(_forward.x, _forward.y, _forward.z, this.DirectionData.w); + } + else if (!this.IsParametric()) + { + _rotation = Quaternion.Inverse(_rotation); + this.DirectionData = new Vector4(_rotation.x, _rotation.y, _rotation.z, _rotation.w); + } + } + + public void UpdateScale() + { + Vector3 _lossyScale = base.get_transform().get_lossyScale(); + if (this.IsAreaLight()) + { + this.SetAreaLight(); + } + this.SquaredScale = (_lossyScale.x + _lossyScale.y + _lossyScale.z) / 3f; + this.SquaredScale *= this.SquaredScale; + this.MarkRangeDirtyAndRequestUpdate(); + } + + public void UpdateTransform() + { + Vector3 _position = base.get_transform().get_position(); + if (this._prevPosition != _position) + { + this._prevPosition = _position; + this.UpdatePosition(); + } + Quaternion _rotation = base.get_transform().get_rotation(); + if (this._prevRotation != _rotation) + { + this._prevRotation = _rotation; + this.UpdateRotation(); + } + Vector3 _lossyScale = base.get_transform().get_lossyScale(); + if (this._prevScale != _lossyScale) + { + this._prevScale = _lossyScale; + this.UpdateScale(); + } + } + } +} \ No newline at end of file diff --git a/red.sim.LightVolumesUdon.csproj b/red.sim.LightVolumesUdon.csproj new file mode 100644 index 0000000..321fafb --- /dev/null +++ b/red.sim.LightVolumesUdon.csproj @@ -0,0 +1,94 @@ + + + + {8F01D4AC-4CB3-4704-9526-A79C2533DBC4} + Debug + AnyCPU + red.sim.LightVolumesUdon + Library + vnetstandard2.1 + + + bin\Debug\ + true + DEBUG;TRACE + false + 4 + full + prompt + AnyCPU + + + bin\Release\ + false + TRACE + true + 4 + pdbonly + prompt + AnyCPU + + + + ./red.sim.LightVolumesUdonReferences/netstandard.dll + + + + + + + ./red.sim.LightVolumesUdonReferences/System.Runtime.dll + + + + + false + false + + + false + false + + + false + false + + + false + false + + + false + false + + + false + false + + + false + false + + + false + false + + + false + false + + + false + false + + + false + false + + + false + false + + + + \ No newline at end of file diff --git a/red.sim.LightVolumesUdon.sln b/red.sim.LightVolumesUdon.sln new file mode 100644 index 0000000..bcae4bc --- /dev/null +++ b/red.sim.LightVolumesUdon.sln @@ -0,0 +1,21 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29728.190 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "red.sim.LightVolumesUdon", "red.sim.LightVolumesUdon.csproj", "{8F01D4AC-4CB3-4704-9526-A79C2533DBC4}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {8F01D4AC-4CB3-4704-9526-A79C2533DBC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8F01D4AC-4CB3-4704-9526-A79C2533DBC4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8F01D4AC-4CB3-4704-9526-A79C2533DBC4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8F01D4AC-4CB3-4704-9526-A79C2533DBC4}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/red.sim.LightVolumesUdon/Main.cs b/red.sim.LightVolumesUdon/Main.cs new file mode 100644 index 0000000..a2877fe --- /dev/null +++ b/red.sim.LightVolumesUdon/Main.cs @@ -0,0 +1,30 @@ +using ABI_RC.Core.Util.AssetFiltering; +using MelonLoader; +using System; +using System.Collections.Generic; +using VRCLightVolumes; + +namespace red.sim.LightVolumesUdon +{ + public class Main : MelonMod + { + public Main() + { + } + + public override void OnInitializeMelon() + { + WorldFilter._Base.Add(typeof(LightVolume)); + WorldFilter._Base.Add(typeof(LightVolumeData)); + WorldFilter._Base.Add(typeof(LightVolumeDataSorter)); + WorldFilter._Base.Add(typeof(LightVolumeInstance)); + WorldFilter._Base.Add(typeof(LightVolumeManager)); + WorldFilter._Base.Add(typeof(LightVolumeSetup)); + WorldFilter._Base.Add(typeof(PointLightVolume)); + WorldFilter._Base.Add(typeof(PointLightVolumeInstance)); + SharedFilter.get_SpawnableWhitelist().Add(typeof(PointLightVolumeInstance)); + SharedFilter.get_SpawnableWhitelist().Add(typeof(PointLightVolume)); + MelonLogger.Msg("Initialized, now whitelisting modded components!"); + } + } +} \ No newline at end of file diff --git a/red.sim.LightVolumesUdon/Properties/AssemblyInfoParams.cs b/red.sim.LightVolumesUdon/Properties/AssemblyInfoParams.cs new file mode 100644 index 0000000..c1c45c2 --- /dev/null +++ b/red.sim.LightVolumesUdon/Properties/AssemblyInfoParams.cs @@ -0,0 +1,11 @@ +using System; + +namespace red.sim.LightVolumesUdon.Properties +{ + internal static class AssemblyInfoParams + { + public const string Version = "1.0.0"; + + public const string Author = "REDSIM , SketchFoxsky"; + } +} \ No newline at end of file diff --git a/red.sim.LightVolumesUdonReferences/System.Runtime.dll b/red.sim.LightVolumesUdonReferences/System.Runtime.dll new file mode 100644 index 0000000000000000000000000000000000000000..34e485e8675358c987be110b71b6c15ee3c55493 GIT binary patch literal 32256 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 literal 0 HcmV?d00001 diff --git a/red.sim.LightVolumesUdonReferences/netstandard.dll b/red.sim.LightVolumesUdonReferences/netstandard.dll new file mode 100644 index 0000000000000000000000000000000000000000..d79a63c65c3416e37909618fea240af118fa8988 GIT binary patch literal 91136 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