summaryrefslogtreecommitdiffstats
path: root/LightVolumes.cginc
diff options
context:
space:
mode:
authoryum <yum.food.vr@gmail.com>2025-05-26 17:30:05 -0700
committeryum <yum.food.vr@gmail.com>2025-05-26 17:30:05 -0700
commited7bb9bbfad50edb88d16005618f2487434d61c4 (patch)
tree136f126d5e148c39f2e90dc520e6c5b7bfa81140 /LightVolumes.cginc
parent5c54a56dd2416aaf82d7f5d56063c0a306574d19 (diff)
Add vrc light volumes, double decal slots
Light volumes: https://github.com/REDSIM/VRCLightVolumes
Diffstat (limited to 'LightVolumes.cginc')
-rw-r--r--LightVolumes.cginc503
1 files changed, 503 insertions, 0 deletions
diff --git a/LightVolumes.cginc b/LightVolumes.cginc
new file mode 100644
index 0000000..d10a190
--- /dev/null
+++ b/LightVolumes.cginc
@@ -0,0 +1,503 @@
+/*
+MIT License
+
+Copyright (c) 2025 RED_SIM
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+
+#ifndef VRC_LIGHT_VOLUMES_INCLUDED
+#define VRC_LIGHT_VOLUMES_INCLUDED
+
+// Are Light Volumes enabled on scene?
+uniform float _UdonLightVolumeEnabled;
+
+// All volumes count in scene
+uniform float _UdonLightVolumeCount;
+
+// Additive volumes max overdraw count
+uniform float _UdonLightVolumeAdditiveMaxOverdraw;
+
+// Additive volumes count
+uniform float _UdonLightVolumeAdditiveCount;
+
+// Should volumes be blended with lightprobes?
+uniform float _UdonLightVolumeProbesBlend;
+
+// Should volumes be with sharp edges when not blending with each other
+uniform float _UdonLightVolumeSharpBounds;
+
+// Main 3D Texture atlas
+uniform sampler3D _UdonLightVolume;
+
+// World to Local (-0.5, 0.5) UVW Matrix
+uniform float4x4 _UdonLightVolumeInvWorldMatrix[32];
+
+// L1 SH components rotation (relative to baked rotation)
+uniform float3 _UdonLightVolumeRotation[64];
+
+// Value that is needed to smoothly blend volumes ( BoundsScale / edgeSmooth )
+uniform float3 _UdonLightVolumeInvLocalEdgeSmooth[32];
+
+// AABB Bounds of islands on the 3D Texture atlas
+uniform float3 _UdonLightVolumeUvw[192];
+
+// Color multiplier (RGB) | If we actually need to rotate L1 components at all (A)
+uniform float4 _UdonLightVolumeColor[32];
+
+// Rotates vector by Matrix 2x3
+float3 LV_MultiplyVectorByMatrix2x3(float3 v, float3 r0, float3 r1) {
+ float3 r2 = cross(r0, r1);
+ return float3(dot(v, r0), dot(v, r1), dot(v, r2));
+}
+
+// Checks if local UVW point is in bounds from -0.5 to +0.5
+bool LV_PointLocalAABB(float3 localUVW){
+ return all(abs(localUVW) <= 0.5);
+}
+
+// Calculates local UVW using volume ID
+float3 LV_LocalFromVolume(uint volumeID, float3 worldPos) {
+ return mul(_UdonLightVolumeInvWorldMatrix[volumeID], float4(worldPos, 1.0)).xyz;
+}
+
+// Samples 3 SH textures and packing them into L1 channels
+void LV_SampleLightVolumeTex(float3 uvw0, float3 uvw1, float3 uvw2, out float3 L0, out float3 L1r, out float3 L1g, out float3 L1b) {
+ // Sampling 3D Atlas
+ float4 tex0 = tex3Dlod(_UdonLightVolume, float4(uvw0, 0));
+ float4 tex1 = tex3Dlod(_UdonLightVolume, float4(uvw1, 0));
+ float4 tex2 = tex3Dlod(_UdonLightVolume, float4(uvw2, 0));
+ // Packing final data
+ L0 = tex0.rgb;
+ L1r = float3(tex1.r, tex2.r, tex0.a);
+ L1g = float3(tex1.g, tex2.g, tex1.a);
+ L1b = float3(tex1.b, tex2.b, tex2.a);
+}
+
+// Bounds mask for a volume rotated in world space, using local UVW
+float LV_BoundsMask(float3 localUVW, float3 invLocalEdgeSmooth) {
+ float3 distToMin = (localUVW + 0.5) * invLocalEdgeSmooth;
+ float3 distToMax = (0.5 - localUVW) * invLocalEdgeSmooth;
+ float3 fade = saturate(min(distToMin, distToMax));
+ return fade.x * fade.y * fade.z;
+}
+
+// Default light probes SH components
+void LV_SampleLightProbe(out float3 L0, out float3 L1r, out float3 L1g, out float3 L1b) {
+ L0 = float3(unity_SHAr.w, unity_SHAg.w, unity_SHAb.w);
+ L1r = unity_SHAr.xyz;
+ L1g = unity_SHAg.xyz;
+ L1b = unity_SHAb.xyz;
+}
+
+// Default light probes L0 only
+float3 LV_SampleLightProbe_L0() {
+ return float3(unity_SHAr.w, unity_SHAg.w, unity_SHAb.w);
+}
+
+// Linear single SH L1 channel evaluation
+float LV_EvaluateSH(float L0, float3 L1, float3 n) {
+ return L0 + dot(L1, n);
+}
+
+// Samples a Volume with ID and Local UVW
+void LV_SampleVolume(uint id, float3 localUVW, out float3 L0, out float3 L1r, out float3 L1g, out float3 L1b) {
+
+ // Additive UVW
+ uint uvwID = id * 6;
+ float3 uvwMin0 = _UdonLightVolumeUvw[uvwID].xyz;
+ float3 uvwScaled = saturate(localUVW + 0.5) * (_UdonLightVolumeUvw[uvwID + 1].xyz - uvwMin0);
+ float3 uvw0 = uvwMin0 + uvwScaled;
+ float3 uvw1 = _UdonLightVolumeUvw[uvwID + 2].xyz + uvwScaled;
+ float3 uvw2 = _UdonLightVolumeUvw[uvwID + 4].xyz + uvwScaled;
+
+ // Sample additive
+ LV_SampleLightVolumeTex(uvw0, uvw1, uvw2, L0, L1r, L1g, L1b);
+
+ // Color correction
+ float4 color = _UdonLightVolumeColor[id];
+ L0 = L0 * color.rgb;
+ L1r = L1r * color.r;
+ L1g = L1g * color.g;
+ L1b = L1b * color.b;
+
+ // Rotate if needed
+ if (color.a != 0) {
+ int id2 = id * 2;
+ float3 r0 = _UdonLightVolumeRotation[id2];
+ float3 r1 = _UdonLightVolumeRotation[id2 + 1];
+ L1r = LV_MultiplyVectorByMatrix2x3(L1r, r0, r1);
+ L1g = LV_MultiplyVectorByMatrix2x3(L1g, r0, r1);
+ L1b = LV_MultiplyVectorByMatrix2x3(L1b, r0, r1);
+ }
+
+}
+
+// Samples a Volume with ID and Local UVW, but L0 component only
+float3 LV_SampleVolume_L0(uint id, float3 localUVW) {
+ uint uvwID = id * 6;
+ float3 uvwMin0 = _UdonLightVolumeUvw[uvwID].xyz;
+ float3 uvw0 = saturate(localUVW + 0.5) * (_UdonLightVolumeUvw[uvwID + 1].xyz - uvwMin0) + uvwMin0;
+ return tex3Dlod(_UdonLightVolume, float4(uvw0, 0)).rgb * _UdonLightVolumeColor[id].rgb;
+}
+
+// Forms specular based on roughness
+float LV_DistributionGGX(float NoH, float roughness) {
+ float f = (roughness - 1) * ((roughness + 1) * (NoH * NoH)) + 1;
+ return (roughness * roughness) / ((float) 3.141592653589793f * f * f);
+}
+
+// Faster normalize
+float3 LV_Normalize(float3 v) {
+ return rsqrt(dot(v, v)) * v;
+}
+
+// Calculates speculars for light volumes or any SH L1 data
+float3 LightVolumeSpecular(float3 f0, float smoothness, float3 worldNormal, float3 viewDir, float3 L0, float3 L1r, float3 L1g, float3 L1b) {
+
+ float3 specColor = max(float3(dot(reflect(-L1r, worldNormal), viewDir), dot(reflect(-L1g, worldNormal), viewDir), dot(reflect(-L1b, worldNormal), viewDir)), 0);
+
+ float3 rDir = LV_Normalize(LV_Normalize(L1r) + viewDir);
+ float3 gDir = LV_Normalize(LV_Normalize(L1g) + viewDir);
+ float3 bDir = LV_Normalize(LV_Normalize(L1b) + viewDir);
+
+ float rNh = saturate(dot(worldNormal, rDir));
+ float gNh = saturate(dot(worldNormal, gDir));
+ float bNh = saturate(dot(worldNormal, bDir));
+
+ float roughness = 1 - smoothness;
+ float roughExp = roughness * roughness;
+
+ float rSpec = LV_DistributionGGX(rNh, roughExp);
+ float gSpec = LV_DistributionGGX(gNh, roughExp);
+ float bSpec = LV_DistributionGGX(bNh, roughExp);
+
+ float3 specs = (rSpec + gSpec + bSpec) * f0;
+ float3 coloredSpecs = specs * specColor;
+
+ float3 a = coloredSpecs + specs * L0;
+ float3 b = coloredSpecs * 4;
+
+ return max(lerp(a, b, smoothness), 0.0);
+
+}
+
+float3 LightVolumeSpecular(float3 albedo, float smoothness, float metallic, float3 worldNormal, float3 viewDir, float3 L0, float3 L1r, float3 L1g, float3 L1b) {
+
+ float3 specularf0 = lerp(0.04f, albedo, metallic);
+ return LightVolumeSpecular(specularf0, smoothness, worldNormal, viewDir, L0, L1r, L1g, L1b);
+}
+
+// Calculates speculars for light volumes or any SH L1 data, but simplified, with only one dominant direction
+float3 LightVolumeSpecularDominant(float3 f0, float smoothness, float3 worldNormal, float3 viewDir, float3 L0, float3 L1r, float3 L1g, float3 L1b) {
+
+ float3 dominantDir = L1r + L1g + L1b;
+ float3 dir = LV_Normalize(LV_Normalize(dominantDir) + viewDir);
+ float nh = saturate(dot(worldNormal, dir));
+
+ float roughness = 1 - smoothness;
+ float roughExp = roughness * roughness;
+
+ float spec = LV_DistributionGGX(nh, roughExp);
+
+ return max(spec * L0 * f0, 0.0) * 2;
+
+}
+
+float3 LightVolumeSpecularDominant(float3 albedo, float smoothness, float metallic, float3 worldNormal, float3 viewDir, float3 L0, float3 L1r, float3 L1g, float3 L1b) {
+
+ float3 specularf0 = lerp(0.04f, albedo, metallic);
+ return LightVolumeSpecularDominant(specularf0, smoothness, worldNormal, viewDir, L0, L1r, L1g, L1b);
+}
+
+// Calculate Light Volume Color based on all SH components provided and the world normal
+float3 LightVolumeEvaluate(float3 worldNormal, float3 L0, float3 L1r, float3 L1g, float3 L1b) {
+ return float3(LV_EvaluateSH(L0.r, L1r, worldNormal), LV_EvaluateSH(L0.g, L1g, worldNormal), LV_EvaluateSH(L0.b, L1b, worldNormal));
+}
+
+// Calculates SH components based on the world position
+void LightVolumeSH(float3 worldPos, out float3 L0, out float3 L1r, out float3 L1g, out float3 L1b) {
+
+ // Initializing output variables
+ L0 = float3(0, 0, 0);
+ L1r = float3(0, 0, 0);
+ L1g = float3(0, 0, 0);
+ L1b = float3(0, 0, 0);
+
+ // Fallback to default light probes if Light Volume are not enabled
+ if (!_UdonLightVolumeEnabled || _UdonLightVolumeCount == 0) {
+ LV_SampleLightProbe(L0, L1r, L1g, L1b);
+ return;
+ }
+
+ uint volumeID_A = -1; // Main, dominant volume ID
+ uint volumeID_B = -1; // Secondary volume ID to blend main with
+
+ float3 localUVW = float3(0, 0, 0); // Last local UVW to use in disabled Light Probes mode
+ float3 localUVW_A = float3(0, 0, 0); // Main local UVW for Y Axis and Free rotations
+ float3 localUVW_B = float3(0, 0, 0); // Secondary local UVW
+
+ // Are A and B volumes NOT found?
+ bool isNoA = true;
+ bool isNoB = true;
+
+ // Additive volumes variables
+ uint addVolumesCount = 0;
+ float3 L0_, L1r_, L1g_, L1b_;
+
+ // Iterating through all light volumes with simplified algorithm requiring Light Volumes to be sorted by weight in descending order
+ [loop]
+ for (uint id = 0; id < (uint) _UdonLightVolumeCount; id++) {
+ localUVW = LV_LocalFromVolume(id, worldPos);
+ if (LV_PointLocalAABB(localUVW)) { // Intersection test
+ if (id < (uint) _UdonLightVolumeAdditiveCount) { // Sampling additive volumes
+ if (addVolumesCount < (uint) _UdonLightVolumeAdditiveMaxOverdraw) {
+ LV_SampleVolume(id, localUVW, L0_, L1r_, L1g_, L1b_);
+ L0 += L0_;
+ L1r += L1r_;
+ L1g += L1g_;
+ L1b += L1b_;
+ addVolumesCount++;
+ }
+ } else if (isNoA) { // First, searching for volume A
+ volumeID_A = id;
+ localUVW_A = localUVW;
+ isNoA = false;
+ } else { // Next, searching for volume B if A found
+ volumeID_B = id;
+ localUVW_B = localUVW;
+ isNoB = false;
+ break;
+ }
+ }
+ }
+
+ // Volume A SH components and mask to blend volume sides
+ float3 L0_A = float3(1, 1, 1);
+ float3 L1r_A = float3(0, 0, 0);
+ float3 L1g_A = float3(0, 0, 0);
+ float3 L1b_A = float3(0, 0, 0);
+
+ // If no volumes found, using Light Probes as fallback
+ if (isNoA && _UdonLightVolumeProbesBlend) {
+ LV_SampleLightProbe(L0_, L1r_, L1g_, L1b_);
+ L0 += L0_;
+ L1r += L1r_;
+ L1g += L1g_;
+ L1b += L1b_;
+ return;
+ }
+
+ // Fallback to lowest weight light volume if outside of every volume
+ localUVW_A = isNoA ? localUVW : localUVW_A;
+ volumeID_A = isNoA ? _UdonLightVolumeCount - 1 : volumeID_A;
+
+ // Sampling Light Volume A
+ LV_SampleVolume(volumeID_A, localUVW_A, L0_A, L1r_A, L1g_A, L1b_A);
+
+ float mask = LV_BoundsMask(localUVW_A, _UdonLightVolumeInvLocalEdgeSmooth[volumeID_A]);
+ if (mask == 1 || isNoA || (_UdonLightVolumeSharpBounds && isNoB)) { // Returning SH A result if it's the center of mask or out of bounds
+ L0 += L0_A;
+ L1r += L1r_A;
+ L1g += L1g_A;
+ L1b += L1b_A;
+ return;
+ }
+
+ // Volume B SH components
+ float3 L0_B = float3(1, 1, 1);
+ float3 L1r_B = float3(0, 0, 0);
+ float3 L1g_B = float3(0, 0, 0);
+ float3 L1b_B = float3(0, 0, 0);
+
+ if (isNoB && _UdonLightVolumeProbesBlend) { // No Volume found and light volumes blending enabled
+
+ // Sample Light Probes B
+ LV_SampleLightProbe(L0_B, L1r_B, L1g_B, L1b_B);
+
+ } else { // Blending Volume A and Volume B
+
+ // If no volume b found, use last one found to fallback
+ localUVW_B = isNoB ? localUVW : localUVW_B;
+ volumeID_B = isNoB ? _UdonLightVolumeCount - 1 : volumeID_B;
+
+ // Sampling Light Volume B
+ LV_SampleVolume(volumeID_B, localUVW_B, L0_B, L1r_B, L1g_B, L1b_B);
+
+ }
+
+ // Lerping SH components
+ L0 += lerp(L0_B, L0_A, mask);
+ L1r += lerp(L1r_B, L1r_A, mask);
+ L1g += lerp(L1g_B, L1g_A, mask);
+ L1b += lerp(L1b_B, L1b_A, mask);
+
+}
+
+// Calculates SH components based on the world position but for additive volumes only
+void LightVolumeAdditiveSH(float3 worldPos, out float3 L0, out float3 L1r, out float3 L1g, out float3 L1b) {
+
+ // Initializing output variables
+ L0 = float3(0, 0, 0);
+ L1r = float3(0, 0, 0);
+ L1g = float3(0, 0, 0);
+ L1b = float3(0, 0, 0);
+
+ if (!_UdonLightVolumeEnabled || _UdonLightVolumeAdditiveCount == 0) return;
+
+ // Additive volumes variables
+ float3 localUVW = float3(0, 0, 0);
+ float3 L0_, L1r_, L1g_, L1b_;
+
+ // Max additive volumes to sample
+ uint count = min((uint) _UdonLightVolumeAdditiveCount, (uint) _UdonLightVolumeAdditiveMaxOverdraw);
+
+ // Iterating through all light volumes with simplified algorithm requiring Light Volumes to be sorted by weight in descending order
+ [loop]
+ for (uint id = 0; id < count; id++) {
+ localUVW = LV_LocalFromVolume(id, worldPos);
+ //Intersection test
+ if (LV_PointLocalAABB(localUVW)) {
+ LV_SampleVolume(id, localUVW, L0_, L1r_, L1g_, L1b_);
+ L0 += L0_;
+ L1r += L1r_;
+ L1g += L1g_;
+ L1b += L1b_;
+ }
+ }
+
+}
+
+// Calculates L0 components based on the world position
+float3 LightVolumeSH_L0(float3 worldPos) {
+
+ // Fallback to default light probes if Light Volume are not enabled
+ if (!_UdonLightVolumeEnabled || _UdonLightVolumeCount == 0) {
+ return LV_SampleLightProbe_L0();
+ }
+
+ float3 L0 = float3(0, 0, 0);
+
+ uint volumeID_A = -1; // Main, dominant volume ID
+ uint volumeID_B = -1; // Secondary volume ID to blend main with
+
+ float3 localUVW = float3(0, 0, 0); // Last local UVW to use in disabled Light Probes mode
+ float3 localUVW_A = float3(0, 0, 0); // Main local UVW for Y Axis and Free rotations
+ float3 localUVW_B = float3(0, 0, 0); // Secondary local UVW
+
+ // Are A and B volumes NOT found?
+ bool isNoA = true;
+ bool isNoB = true;
+
+ // Additive volumes variables
+ uint addVolumesCount = 0;
+
+ // Iterating through all light volumes with simplified algorithm requiring Light Volumes to be sorted by weight in descending order
+ [loop]
+ for (uint id = 0; id < (uint) _UdonLightVolumeCount; id++) {
+ localUVW = LV_LocalFromVolume(id, worldPos);
+ if (LV_PointLocalAABB(localUVW)) { // Intersection test
+ if (id < (uint) _UdonLightVolumeAdditiveCount) { // Sampling additive volumes
+ if (addVolumesCount < (uint) _UdonLightVolumeAdditiveMaxOverdraw) {
+ L0 += LV_SampleVolume_L0(id, localUVW);
+ addVolumesCount++;
+ }
+ } else if (isNoA) { // First, searching for volume A
+ volumeID_A = id;
+ localUVW_A = localUVW;
+ isNoA = false;
+ } else { // Next, searching for volume B if A found
+ volumeID_B = id;
+ localUVW_B = localUVW;
+ isNoB = false;
+ break;
+ }
+ }
+ }
+
+ // If no volumes found, using Light Probes as fallback
+ if (isNoA && _UdonLightVolumeProbesBlend) {
+ return L0 + LV_SampleLightProbe_L0();
+ }
+
+ // Fallback to lowest weight light volume if outside of every volume
+ localUVW_A = isNoA ? localUVW : localUVW_A;
+ volumeID_A = isNoA ? _UdonLightVolumeCount - 1 : volumeID_A;
+
+ // Sampling Light Volume A
+ float3 L0_A = LV_SampleVolume_L0(volumeID_A, localUVW_A);
+
+ float mask = LV_BoundsMask(localUVW_A, _UdonLightVolumeInvLocalEdgeSmooth[volumeID_A]);
+ if (mask == 1 || isNoA || (_UdonLightVolumeSharpBounds && isNoB)) { // Returning SH A result if it's the center of mask or out of bounds
+ return L0 + L0_A;
+ }
+
+ // Volume B L0
+ float3 L0_B = float3(1, 1, 1);
+
+ if (isNoB && _UdonLightVolumeProbesBlend) { // No Volume found and light volumes blending enabled
+
+ // Sample Light Probes B
+ L0_B = LV_SampleLightProbe_L0();
+
+ } else { // Blending Volume A and Volume B
+
+ // If no volume b found, use last one found to fallback
+ localUVW_B = isNoB ? localUVW : localUVW_B;
+ volumeID_B = isNoB ? _UdonLightVolumeCount - 1 : volumeID_B;
+
+ // Sampling Light Volume B
+ L0_B = LV_SampleVolume_L0(volumeID_B, localUVW_B);
+
+ }
+
+ // Lerping L0
+ return L0 + lerp(L0_B, L0_A, mask);
+
+}
+
+// Calculates L0 component based on the world position but for additive volumes only
+float3 LightVolumeAdditiveSH_L0(float3 worldPos) {
+
+ // Initializing output variables
+ float3 L0 = float3(0, 0, 0);
+
+ if (!_UdonLightVolumeEnabled || _UdonLightVolumeAdditiveCount == 0) return L0;
+
+ // Additive volumes variables
+ float3 localUVW = float3(0, 0, 0);
+
+ // Max additive volumes to sample
+ uint count = min((uint) _UdonLightVolumeAdditiveCount, (uint) _UdonLightVolumeAdditiveMaxOverdraw);
+
+ // Iterating through all light volumes with simplified algorithm requiring Light Volumes to be sorted by weight in descending order
+ [loop]
+ for (uint id = 0; id < count; id++) {
+ localUVW = LV_LocalFromVolume(id, worldPos);
+ //Intersection test
+ if (LV_PointLocalAABB(localUVW)) {
+ L0 += LV_SampleVolume_L0(id, localUVW);
+ }
+ }
+
+ return L0;
+
+}
+
+#endif