Skip to content

Commit 6012f07

Browse files
committed
Dynamic GI Spherical Harmonic from Spherical Gaussian Modes (#9)
* Implemented SH from SG Modes which can be toggled between in the Probe Dynamic GI volume settings. SamplePeakAndProject is the same mode we are used to: Spherical gaussians will simply be evaluated at their peak and projected to convert to spherical harmonics. SHFromSGFit: A spherical gaussian to spherical harmonic function fit is used, which is physically plausible. SHFromSGFitWithCosineWindow: A spherical gaussian with an additional cosine window to spherical harmonic function fit is used, which is physically plausible. Less directional blur than SHFromSGFit. * Cleanup pass on SG to SH supporting math. Created Zonal Harmonic data type with supporting functions to add type saftey to the transforms as we have done with the other spherical harmonic functions. Created specialized zonal harmonic rotation functions which significantly reduces the work required for rotation. It's possible the compiler was handling this already by stripping out work on zero coefficients, and by automatically using floats instead of float3s when all channels are the same - but better to not put too much pressure on the compiler. This should also make it easier in the future to create specialized rotation functions for cardinal rotations (which will introduce additional zeros). Co-authored-by: pastasfuture <[email protected]>
1 parent db1102d commit 6012f07

File tree

5 files changed

+445
-173
lines changed

5 files changed

+445
-173
lines changed

com.unity.render-pipelines.high-definition/Runtime/Lighting/ProbeVolume/DynamicGI/ProbeDynamicGI.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ public class ProbeDynamicGI : VolumeComponent
3535
public ClampedFloatParameter propagationSharpness = new ClampedFloatParameter(2.0f, 0.0f, 16.0f);
3636
[Tooltip("Advanced control for the SG sharpness used when evaluating the influence of infinite bounce light near surfaces")]
3737
public ClampedFloatParameter infiniteBounceSharpness = new ClampedFloatParameter(2.0f, 0.0f, 16.0f);
38+
[Tooltip("Advanced control for probe propagation combine pass.\nSamplePeakAndProject: Spherical gaussians will simply be evaluated at their peak and projected to convert to spherical harmonics.\nSHFromSGFit: A spherical gaussian to spherical harmonic function fit is used, which is physically plausible.\nSHFromSGFitWithCosineWindow: A spherical gaussian with an additional cosine window to spherical harmonic function fit is used, which is physically plausible. Less directional blur than SHFromSGFit.")]
39+
public SHFromSGModeParameter shFromSGMode = new SHFromSGModeParameter(SHFromSGMode.SamplePeakAndProject);
3840
[Tooltip("Advanced control for darkening down the indirect light on invalid probes")]
3941
public ClampedFloatParameter leakMultiplier = new ClampedFloatParameter(0.0f, 0.0f, 1.0f);
4042
[Tooltip("Advanced control to bias the distance from the normal of the hit surface to perform direct lighting evaluation on")]
@@ -47,5 +49,20 @@ public class ProbeDynamicGI : VolumeComponent
4749

4850
[Tooltip("Advanced control to clear all dynamic GI buffers in the event lighting blows up when tuning")]
4951
public BoolParameter clear = new BoolParameter(false);
52+
53+
[Serializable]
54+
public enum SHFromSGMode
55+
{
56+
SamplePeakAndProject = 0,
57+
SHFromSGFit,
58+
SHFromSGFitWithCosineWindow
59+
};
60+
61+
[Serializable]
62+
public sealed class SHFromSGModeParameter : VolumeParameter<SHFromSGMode>
63+
{
64+
public SHFromSGModeParameter(SHFromSGMode value, bool overrideState = false)
65+
: base(value, overrideState) {}
66+
}
5067
}
5168
} // UnityEngine.Experimental.Rendering.HDPipeline

com.unity.render-pipelines.high-definition/Runtime/Lighting/ProbeVolume/DynamicGI/ProbePropagationCombine.compute

Lines changed: 102 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
#include "Packages/com.unity.render-pipelines.high-definition-config/Runtime/ShaderConfig.cs.hlsl"
22
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/EntityLighting.hlsl"
3+
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/CommonLighting.hlsl"
34
#include "Packages/com.unity.render-pipelines.high-definition/Runtime/Lighting/ProbeVolume/ProbeVolumeRotate.hlsl"
45
#include "Packages/com.unity.render-pipelines.high-definition/Runtime/Lighting/ProbeVolume/DynamicGI/ProbePropagationGlobals.hlsl"
56
#include "Packages/com.unity.render-pipelines.high-definition/Runtime/Lighting/ProbeVolume/DynamicGI/ProbeVolumeSphericalHarmonicsLighting.hlsl"
67
#include "Packages/com.unity.render-pipelines.high-definition/Runtime/Lighting/ProbeVolume/DynamicGI/ProbeVolumeSphericalHarmonicsDeringing.hlsl"
78

89
#pragma kernel CombinePropagationAxis
910

11+
#pragma multi_compile _ SH_FROM_SG_PBR_FIT SH_FROM_SG_PBR_FIT_WITH_COSINE_WINDOW
12+
#pragma multi_compile _
13+
1014
#define GROUP_SIZE 64
1115
//#pragma enable_d3d11_debug_symbols
1216

@@ -30,6 +34,8 @@ float _BakedLightingContribution;
3034
float _DynamicPropagationContribution;
3135
float4 _RayAxis[NEIGHBOR_AXIS_COUNT];
3236

37+
float _PropagationSharpness;
38+
3339

3440
uint3 ComputeWriteIndexFromReadIndex(uint readIndex, float3 resolution)
3541
{
@@ -116,8 +122,7 @@ void WriteFinalSHOutgoingRadiosityWithProjectedConstantsPacked(uint3 writeIndex,
116122
_ProbeVolumeAtlasWriteTextureSH[uint3(writeIndex.x, writeIndex.y, writeIndex.z + _ProbeVolumeAtlasResolutionAndSliceCount.z * 6)] = float4(outgoingRadiosityProjectedConstantsPacked.data[6].xyz, validity);
117123
}
118124

119-
120-
SHIncomingIrradiance ProjectPropagationAxis(uint probeIndex)
125+
SHIncomingIrradiance ProjectPropagationAxisFromPeak(uint probeIndex)
121126
{
122127
SHIncomingIrradiance incomingIrradiance;
123128
ZERO_INITIALIZE(SHIncomingIrradiance, incomingIrradiance);
@@ -133,7 +138,102 @@ SHIncomingIrradiance ProjectPropagationAxis(uint probeIndex)
133138
}
134139

135140
return incomingIrradiance;
141+
}
142+
143+
// data for fit generated with: https://gist.github.com/pastasfuture/e1a7d80d6ed1104540b22edc15ce655a
144+
// Fit coefficient function fit generated in desmos (for non zero parameters c0, c2, and c6): https://www.desmos.com/calculator/rnrmjlz1jb
145+
// Reference implementation: https://www.shadertoy.com/view/7tsXzH
146+
//
147+
// Note: These ComputeSphericalHarmonicFromSphericalGaussian functions can be replaced with hardcoded constants once we make sharpness hardcoded.
148+
#if !defined(SH_FROM_SG_PBR_FIT_WITH_COSINE_WINDOW)
149+
float ZHWindowComputeFromSphericalGaussianC0(float sharpness)
150+
{
151+
return pow(sharpness * 1.22962 + 0.823224, -1.25462) * 3.73236 + 0.0289582;
152+
}
153+
154+
float ZHWindowComputeFromSphericalGaussianC1(float sharpness)
155+
{
156+
return exp2(-3.78299 * pow(abs(sharpness * 0.766922 + -0.687697), -0.95733)) * -0.862788 + 0.851292;
157+
}
158+
159+
float ComputeSigmoidSCurve(float x, float p)
160+
{
161+
float a = exp2(x * p);
162+
return a / (a + 1.0);
163+
}
136164

165+
float ZHWindowComputeFromSphericalGaussianC2(float sharpness)
166+
{
167+
return lerp(
168+
sharpness * sharpness * -0.0326676 + sharpness * 0.225077,
169+
ComputeSigmoidSCurve(sharpness * 15.3784 + -31.6152, 0.0162406) * -0.707591 + 0.85572,
170+
saturate((sharpness - 0.800167) / (0.800167 - 4.57034))
171+
);
172+
}
173+
#else
174+
float ZHWindowComputeFromSphericalGaussianC0(float sharpness)
175+
{
176+
return pow(sharpness * 0.386254 + 1.55848, -1.52661) * 1.72542 + 0.0290483;
177+
}
178+
179+
float ZHWindowComputeFromSphericalGaussianC1(float sharpness)
180+
{
181+
return exp2(-8.6001 * pow(abs(sharpness * 1.53466 + 3.47572), -1.16397)) * -1.32848 + 1.34496;
182+
}
183+
184+
float ZHWindowComputeFromSphericalGaussianC2(float sharpness)
185+
{
186+
return exp2(-0.506072 * pow(abs(sharpness * 0.273738 + 0.296201), -1.06396)) * -0.506072 + 0.511033;
187+
}
188+
#endif
189+
190+
ZHWindow ZHWindowComputeFromSphericalGaussian(float sharpness)
191+
{
192+
ZHWindow zhWindow;
193+
zhWindow.data[0] = ZHWindowComputeFromSphericalGaussianC0(sharpness);
194+
zhWindow.data[1] = ZHWindowComputeFromSphericalGaussianC1(sharpness);
195+
zhWindow.data[2] = ZHWindowComputeFromSphericalGaussianC2(sharpness);
196+
197+
return zhWindow;
198+
}
199+
200+
SHIncomingIrradiance SHIncomingIrradianceComputeFromSphericalGaussian(float3 direction, float sharpness, float3 radiance)
201+
{
202+
ZHWindow zhWindow = ZHWindowComputeFromSphericalGaussian(sharpness);
203+
SHWindow shWindow = SHWindowComputeFromZHWindow(zhWindow, direction);
204+
SHIncomingIrradiance irradiance = SHIncomingIrradianceComputeFromSHWindowAndRadiance(shWindow, radiance);
205+
206+
return irradiance;
207+
}
208+
209+
SHIncomingIrradiance ProjectPropagationAxisFromFit(uint probeIndex)
210+
{
211+
SHIncomingIrradiance incomingIrradiance;
212+
ZERO_INITIALIZE(SHIncomingIrradiance, incomingIrradiance);
213+
214+
uint localIndex = probeIndex * NEIGHBOR_AXIS_COUNT;
215+
for (int axis = 0; axis < NEIGHBOR_AXIS_COUNT; ++axis)
216+
{
217+
float3 radiance = _RadianceCacheAxis[localIndex].xyz;
218+
float3 direction = _RayAxis[axis].xyz;
219+
220+
SHIncomingIrradiance incomingIrradianceCurrentSG = SHIncomingIrradianceComputeFromSphericalGaussian(direction, _PropagationSharpness, radiance);
221+
222+
SHIncomingIrradianceAccumulateFromSHIncomingIrradiance(incomingIrradiance, incomingIrradianceCurrentSG);
223+
224+
localIndex++;
225+
}
226+
227+
return incomingIrradiance;
228+
}
229+
230+
SHIncomingIrradiance ProjectPropagationAxis(uint probeIndex)
231+
{
232+
#if defined(SH_FROM_SG_PBR_FIT) || defined(SH_FROM_SG_PBR_FIT_WITH_COSINE_WINDOW)
233+
return ProjectPropagationAxisFromFit(probeIndex);
234+
#else
235+
return ProjectPropagationAxisFromPeak(probeIndex);
236+
#endif
137237
}
138238

139239

@@ -168,4 +268,3 @@ void CombinePropagationAxis(uint3 id : SV_DispatchThreadID)
168268
WriteFinalSHOutgoingRadiosityWithProjectedConstantsPacked(writeIndex, bakedOutgoingRadiosityProjectedConstantsPacked, validity);
169269
}
170270
}
171-

com.unity.render-pipelines.high-definition/Runtime/Lighting/ProbeVolume/DynamicGI/ProbeVolumeDynamicGI.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,30 @@ void DispatchPropagationCombine(CommandBuffer cmd, ProbeVolumeHandle probeVolume
480480
}
481481
cmd.SetComputeVectorArrayParam(shader, "_RayAxis", s_NeighborAxis);
482482

483+
cmd.SetComputeFloatParam(shader, "_PropagationSharpness", giSettings.propagationSharpness.value);
484+
485+
switch (giSettings.shFromSGMode.value)
486+
{
487+
case ProbeDynamicGI.SHFromSGMode.SamplePeakAndProject:
488+
{
489+
CoreUtils.SetKeyword(shader, "SH_FROM_SG_PBR_FIT", false);
490+
CoreUtils.SetKeyword(shader, "SH_FROM_SG_PBR_FIT_WITH_COSINE_WINDOW", false);
491+
break;
492+
}
493+
case ProbeDynamicGI.SHFromSGMode.SHFromSGFit:
494+
{
495+
CoreUtils.SetKeyword(shader, "SH_FROM_SG_PBR_FIT", true);
496+
CoreUtils.SetKeyword(shader, "SH_FROM_SG_PBR_FIT_WITH_COSINE_WINDOW", false);
497+
break;
498+
}
499+
case ProbeDynamicGI.SHFromSGMode.SHFromSGFitWithCosineWindow:
500+
{
501+
CoreUtils.SetKeyword(shader, "SH_FROM_SG_PBR_FIT", false);
502+
CoreUtils.SetKeyword(shader, "SH_FROM_SG_PBR_FIT_WITH_COSINE_WINDOW", true);
503+
break;
504+
}
505+
default: break;
506+
}
483507

484508
int dispatchX = (numProbes + 63) / 64;
485509
cmd.DispatchCompute(shader, kernel, dispatchX, 1, 1);

com.unity.render-pipelines.high-definition/Runtime/Lighting/ProbeVolume/DynamicGI/ProbeVolumeSphericalHarmonicsDeringing.hlsl

Lines changed: 1 addition & 170 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,6 @@ float ComputeLuminance(float3 color)
1111
return dot(color, float3(0.25, 0.5, 0.25));
1212
}
1313

14-
// Dering the 9x float3 SH coefficients, and write out the corresponding shCoefficientIndex for the current pixel.
15-
// Needed for isolating individual color channels in deringing code.
16-
struct SHOutgoingRadiosityScalar
17-
{
18-
float data[SH_COEFFICIENT_COUNT];
19-
};
20-
2114
SHOutgoingRadiosityScalar SHOutgoingRadiosityReadColorChannel(SHOutgoingRadiosity shOutgoingRadiosity, int colorChannelIndex)
2215
{
2316
SHOutgoingRadiosityScalar shOutgoingRadiosityScalar;
@@ -46,168 +39,6 @@ void SHOutgoingRadiosityWriteColorChannel(inout SHOutgoingRadiosity shOutgoingRa
4639
}
4740
}
4841

49-
void SHOutgoingRadiosityScalarRotateBand1(float3x3 M, inout float x[3])
50-
{
51-
float3 SH = float3(-x[2], -x[0], x[1]);
52-
53-
x[0] = dot(SH, -float3(M[0][1], M[1][1], M[2][1]));
54-
x[1] = dot(SH, float3(M[0][2], M[1][2], M[2][2]));
55-
x[2] = dot(SH, -float3(M[0][0], M[1][0], M[2][0]));
56-
}
57-
58-
void SHOutgoingRadiosityScalarRotateBand2(float3x3 M, inout float x[5])
59-
{
60-
// Decomposed + factored version of 5x5 matrix multiply of invA * sh from source.
61-
const float k0 = 0.9152912328637689;
62-
const float k1 = 0.9152912328637689 * 2.0;
63-
const float k2 = 1.5853309190424043;
64-
float sh0 = x[1] * -0.5 + (x[3] * 0.5 + x[4]); // 2x MADD
65-
float sh1 = (x[0] + (k2 / k0) * x[2] + x[3] + x[4]) * 0.5;
66-
float sh2 = x[0];
67-
float sh3 = -x[3];
68-
float sh4 = -x[1];
69-
70-
const float k = 1.0 / sqrt(2.0);
71-
const float kInv = sqrt(2.0);
72-
const float k3 = k0 * 2.0 * K3SQRT5DIV4SQRTPI * k * k; // sqrt(3.0) / 2.0
73-
const float k4 = k0 * 2.0 * -KALMOSTONETHIRD;
74-
75-
// Decomposed + factored version of 5x5 matrix multiply of 5 normals projected to 5 SH2 bands.
76-
// Column 0
77-
{
78-
float3 rn0 = float3(M[0][0], M[0][1], M[0][2]) * kInv; // (float3(1, 0, 0) * M) / k;
79-
x[0] = (rn0.x * rn0.y) * sh0;
80-
x[1] = (-rn0.y * rn0.z) * sh0;
81-
x[2] = (rn0.z * rn0.z * k3 + k4) * sh0;
82-
x[3] = (-rn0.x * rn0.z) * sh0;
83-
x[4] = (rn0.x * rn0.x - rn0.y * rn0.y) * sh0;
84-
}
85-
86-
// Column 1
87-
{
88-
float3 rn1 = float3(M[2][0], M[2][1], M[2][2]) * kInv; // (float3(0, 0, 1) * M) / k;
89-
x[0] += (rn1.x * rn1.y) * sh1;
90-
x[1] += (-rn1.y * rn1.z) * sh1;
91-
x[2] += (rn1.z * rn1.z * k3 + k4) * sh1;
92-
x[3] += (-rn1.x * rn1.z) * sh1;
93-
x[4] += (rn1.x * rn1.x - rn1.y * rn1.y) * sh1;
94-
}
95-
96-
// Column 2
97-
{
98-
float3 rn2 = float3(M[0][0] + M[1][0], M[0][1] + M[1][1], M[0][2] + M[1][2]); // (float3(k, k, 0) * M) / k;
99-
x[0] += (rn2.x * rn2.y) * sh2;
100-
x[1] += (-rn2.y * rn2.z) * sh2;
101-
x[2] += (rn2.z * rn2.z * k3 + k4) * sh2;
102-
x[3] += (-rn2.x * rn2.z) * sh2;
103-
x[4] += (rn2.x * rn2.x - rn2.y * rn2.y) * sh2;
104-
}
105-
106-
// Column 3
107-
{
108-
float3 rn3 = float3(M[0][0] + M[2][0], M[0][1] + M[2][1], M[0][2] + M[2][2]); // (float3(k, 0, k) * M) / k;
109-
x[0] += (rn3.x * rn3.y) * sh3;
110-
x[1] += (-rn3.y * rn3.z) * sh3;
111-
x[2] += (rn3.z * rn3.z * k3 + k4) * sh3;
112-
x[3] += (-rn3.x * rn3.z) * sh3;
113-
x[4] += (rn3.x * rn3.x - rn3.y * rn3.y) * sh3;
114-
}
115-
116-
// Column 4
117-
{
118-
float3 rn4 = float3(M[1][0] + M[2][0], M[1][1] + M[2][1], M[1][2] + M[2][2]); // (float3(0, k, k) * M) / k;
119-
x[0] += (rn4.x * rn4.y) * sh4;
120-
x[1] += (-rn4.y * rn4.z) * sh4;
121-
x[2] += (rn4.z * rn4.z * k3 + k4) * sh4;
122-
x[3] += (-rn4.x * rn4.z) * sh4;
123-
x[4] += (rn4.x * rn4.x - rn4.y * rn4.y) * sh4;
124-
}
125-
126-
x[4] *= 0.5;
127-
}
128-
129-
void SHOutgoingRadiosityScalarRotate(float3x3 M, inout SHOutgoingRadiosityScalar shOutgoingRadiosityScalar)
130-
{
131-
float x1[3];
132-
x1[0] = shOutgoingRadiosityScalar.data[1];
133-
x1[1] = shOutgoingRadiosityScalar.data[2];
134-
x1[2] = shOutgoingRadiosityScalar.data[3];
135-
SHOutgoingRadiosityScalarRotateBand1(M, x1);
136-
float x2[5];
137-
x2[0] = shOutgoingRadiosityScalar.data[4];
138-
x2[1] = shOutgoingRadiosityScalar.data[5];
139-
x2[2] = shOutgoingRadiosityScalar.data[6];
140-
x2[3] = shOutgoingRadiosityScalar.data[7];
141-
x2[4] = shOutgoingRadiosityScalar.data[8];
142-
SHOutgoingRadiosityScalarRotateBand2(M, x2);
143-
shOutgoingRadiosityScalar.data[1] = x1[0];
144-
shOutgoingRadiosityScalar.data[2] = x1[1];
145-
shOutgoingRadiosityScalar.data[3] = x1[2];
146-
shOutgoingRadiosityScalar.data[4] = x2[0];
147-
shOutgoingRadiosityScalar.data[5] = x2[1];
148-
shOutgoingRadiosityScalar.data[6] = x2[2];
149-
shOutgoingRadiosityScalar.data[7] = x2[3];
150-
shOutgoingRadiosityScalar.data[8] = x2[4];
151-
}
152-
153-
// optimal linear direction, related to bent normal, etc.
154-
float3 SHOutgoingRadiosityScalarGetOptimalLinear(const SHOutgoingRadiosityScalar shOutgoingRadiosityScalar)
155-
{
156-
return float3(-shOutgoingRadiosityScalar.data[3], -shOutgoingRadiosityScalar.data[1], shOutgoingRadiosityScalar.data[2]);
157-
}
158-
159-
// source: Building an Orthonormal Basis, Revisited
160-
// http://jcgt.org/published/0006/01/01/
161-
// Same as reference implementation, except transposed.
162-
float3x3 ComputeTangentToWorldMatrix(float3 n)
163-
{
164-
float3x3 res;
165-
res[2][0] = n.x;
166-
res[2][1] = n.y;
167-
res[2][2] = n.z;
168-
169-
float s = (n.z >= 0.0f) ? 1.0f : -1.0f;
170-
float a = -1.0f / (s + n.z);
171-
float b = n.x * n.y * a;
172-
173-
res[0][0] = 1.0f + s * n.x * n.x * a;
174-
res[0][1] = s * b;
175-
res[0][2] = -s * n.x;
176-
177-
res[1][0] = b;
178-
res[1][1] = s + n.y * n.y * a;
179-
res[1][2] = -n.y;
180-
181-
return res;
182-
}
183-
184-
void FrameFromNormal(float3 normal, out float3 tangent, out float3 binormal)
185-
{
186-
#if 0
187-
// PPSloan version:
188-
if (abs(normal.x) > abs(normal.z))
189-
{
190-
binormal.x = -normal.y;
191-
binormal.y = normal.x;
192-
binormal.z = 0.0f;
193-
}
194-
else
195-
{
196-
binormal.x = 0.0f;
197-
binormal.y = -normal.z;
198-
binormal.z = normal.y;
199-
}
200-
201-
binormal = normalize(binormal);
202-
tangent = cross(binormal, normal);
203-
#else
204-
float3x3 tangentToWorldMatrix = ComputeTangentToWorldMatrix(normal);
205-
float3x3 worldToTangentMatrix = transpose(transpose(tangentToWorldMatrix)); // TODO: Test this transpose - from the conversion from glsl to hlsl
206-
binormal = worldToTangentMatrix[1];
207-
tangent = worldToTangentMatrix[0];
208-
#endif
209-
}
210-
21142
// a * z^2 + b * z + c + ce is the function, we add the window parameters l2, l1
21243
// l2 * a * z^2 + l1 * b * z + l2 *c + c3, and want to make sure we make the minimum zero (or some positive epsilon)
21344
float ComputeMinError(const float a, const float b, const float c, inout float zmin)
@@ -569,7 +400,7 @@ void SHZHDeRingFull(float zh[3], inout float4 window, const float q1err, const f
569400

570401
void SHOutgoingRadiosityScalarComputeWindow(const SHOutgoingRadiosityScalar shOutgoingRadiosityScalar, inout float4 window)
571402
{
572-
float3 optLin = SHOutgoingRadiosityScalarGetOptimalLinear(shOutgoingRadiosityScalar);
403+
float3 optLin = SHOutgoingRadiosityScalarGetOptimalLinearDirection(shOutgoingRadiosityScalar);
573404

574405
float vecMag = sqrt(dot(optLin, optLin));
575406

0 commit comments

Comments
 (0)