A game about forced loneliness, made by TACStudios
1#ifndef UNITY_IMAGE_BASED_LIGHTING_HLSL_INCLUDED
2#define UNITY_IMAGE_BASED_LIGHTING_HLSL_INCLUDED
3
4#if SHADER_API_MOBILE || SHADER_API_GLES3 || SHADER_API_SWITCH || defined(UNITY_UNIFIED_SHADER_PRECISION_MODEL)
5#pragma warning (disable : 3205) // conversion of larger type to smaller
6#endif
7
8#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/CommonLighting.hlsl"
9#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/CommonMaterial.hlsl"
10#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/BSDF.hlsl"
11#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Random.hlsl"
12#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Sampling/Sampling.hlsl"
13
14#ifndef UNITY_SPECCUBE_LOD_STEPS
15 // This is actuall the last mip index, we generate 7 mips of convolution
16 #define UNITY_SPECCUBE_LOD_STEPS 6
17#endif
18
19//-----------------------------------------------------------------------------
20// Util image based lighting
21//-----------------------------------------------------------------------------
22
23// The *approximated* version of the non-linear remapping. It works by
24// approximating the cone of the specular lobe, and then computing the MIP map level
25// which (approximately) covers the footprint of the lobe with a single texel.
26// Improves the perceptual roughness distribution.
27real PerceptualRoughnessToMipmapLevel(real perceptualRoughness, uint maxMipLevel)
28{
29 perceptualRoughness = perceptualRoughness * (1.7 - 0.7 * perceptualRoughness);
30
31 return perceptualRoughness * maxMipLevel;
32}
33
34real PerceptualRoughnessToMipmapLevel(real perceptualRoughness)
35{
36 return PerceptualRoughnessToMipmapLevel(perceptualRoughness, UNITY_SPECCUBE_LOD_STEPS);
37}
38
39// The *accurate* version of the non-linear remapping. It works by
40// approximating the cone of the specular lobe, and then computing the MIP map level
41// which (approximately) covers the footprint of the lobe with a single texel.
42// Improves the perceptual roughness distribution and adds reflection (contact) hardening.
43// TODO: optimize!
44real PerceptualRoughnessToMipmapLevel(real perceptualRoughness, real NdotR)
45{
46 real m = PerceptualRoughnessToRoughness(perceptualRoughness);
47
48 // Remap to spec power. See eq. 21 in --> https://dl.dropboxusercontent.com/u/55891920/papers/mm_brdf.pdf
49 real n = (2.0 / max(REAL_EPS, m * m)) - 2.0;
50
51 // Remap from n_dot_h formulation to n_dot_r. See section "Pre-convolved Cube Maps vs Path Tracers" --> https://s3.amazonaws.com/docs.knaldtech.com/knald/1.0.0/lys_power_drops.html
52 n /= (4.0 * max(NdotR, REAL_EPS));
53
54 // remap back to square root of real roughness (0.25 include both the sqrt root of the conversion and sqrt for going from roughness to perceptualRoughness)
55 perceptualRoughness = pow(2.0 / (n + 2.0), 0.25);
56
57 return perceptualRoughness * UNITY_SPECCUBE_LOD_STEPS;
58}
59
60// The inverse of the *approximated* version of perceptualRoughnessToMipmapLevel().
61real MipmapLevelToPerceptualRoughness(real mipmapLevel)
62{
63 real perceptualRoughness = saturate(mipmapLevel / UNITY_SPECCUBE_LOD_STEPS);
64
65 return saturate(1.7 / 1.4 - sqrt(2.89 / 1.96 - (2.8 / 1.96) * perceptualRoughness));
66}
67
68//-----------------------------------------------------------------------------
69// Anisotropic image based lighting
70//-----------------------------------------------------------------------------
71
72// T is the fiber axis (hair strand direction, root to tip).
73float3 ComputeViewFacingNormal(float3 V, float3 T)
74{
75 return Orthonormalize(V, T);
76}
77
78// Fake anisotropy by distorting the normal (non-negative anisotropy values only).
79// The grain direction (e.g. hair or brush direction) is assumed to be orthogonal to N.
80// Anisotropic ratio (0->no isotropic; 1->full anisotropy in tangent direction)
81real3 GetAnisotropicModifiedNormal(real3 grainDir, real3 N, real3 V, real anisotropy)
82{
83 real3 grainNormal = ComputeViewFacingNormal(V, grainDir);
84 return normalize(lerp(N, grainNormal, anisotropy));
85}
86
87// For GGX aniso and IBL we have done an empirical (eye balled) approximation compare to the reference.
88// We use a single fetch, and we stretch the normal to use based on various criteria.
89// result are far away from the reference but better than nothing
90// Anisotropic ratio (0->no isotropic; 1->full anisotropy in tangent direction) - positive use bitangentWS - negative use tangentWS
91// Note: returned iblPerceptualRoughness shouldn't be use for sampling FGD texture in a pre-integration
92void GetGGXAnisotropicModifiedNormalAndRoughness(real3 bitangentWS, real3 tangentWS, real3 N, real3 V, real anisotropy, real perceptualRoughness, out real3 iblN, out real iblPerceptualRoughness)
93{
94 // For positive anisotropy values: tangent = highlight stretch (anisotropy) direction, bitangent = grain (brush) direction.
95 float3 grainDirWS = (anisotropy >= 0.0) ? bitangentWS : tangentWS;
96 // Reduce stretching depends on the perceptual roughness
97 float stretch = abs(anisotropy) * saturate(1.5 * sqrt(perceptualRoughness));
98 // NOTE: If we follow the theory we should use the modified normal for the different calculation implying a normal (like NdotV)
99 // However modified normal is just a hack. The goal is just to stretch a cubemap, no accuracy here. Let's save performance instead.
100 iblN = GetAnisotropicModifiedNormal(grainDirWS, N, V, stretch);
101 iblPerceptualRoughness = perceptualRoughness * saturate(1.2 - abs(anisotropy));
102}
103
104// Ref: "Moving Frostbite to PBR", p. 69.
105real3 GetSpecularDominantDir(real3 N, real3 R, real perceptualRoughness, real NdotV)
106{
107 real p = perceptualRoughness;
108 real a = 1.0 - p * p;
109 real s = sqrt(a);
110
111#ifdef USE_FB_DSD
112 // This is the original formulation.
113 real lerpFactor = (s + p * p) * a;
114#else
115 // TODO: tweak this further to achieve a closer match to the reference.
116 real lerpFactor = (s + p * p) * saturate(a * a + lerp(0.0, a, NdotV * NdotV));
117#endif
118
119 // The result is not normalized as we fetch in a cubemap
120 return lerp(N, R, lerpFactor);
121}
122
123// ----------------------------------------------------------------------------
124// Importance sampling BSDF functions
125// ----------------------------------------------------------------------------
126
127void SampleGGXDir(real2 u,
128 real3 V,
129 real3x3 localToWorld,
130 real roughness,
131 out real3 L,
132 out real NdotL,
133 out real NdotH,
134 out real VdotH,
135 bool VeqN = false)
136{
137 // GGX NDF sampling
138 real cosTheta = sqrt(SafeDiv(1.0 - u.x, 1.0 + (roughness * roughness - 1.0) * u.x));
139 real phi = TWO_PI * u.y;
140
141 real3 localH = SphericalToCartesian(phi, cosTheta);
142
143 NdotH = cosTheta;
144
145 real3 localV;
146
147 if (VeqN)
148 {
149 // localV == localN
150 localV = real3(0.0, 0.0, 1.0);
151 VdotH = NdotH;
152 }
153 else
154 {
155 localV = mul(V, transpose(localToWorld));
156 VdotH = saturate(dot(localV, localH));
157 }
158
159 // Compute { localL = reflect(-localV, localH) }
160 real3 localL = -localV + 2.0 * VdotH * localH;
161 NdotL = localL.z;
162
163 L = mul(localL, localToWorld);
164}
165
166// ref: http://blog.selfshadow.com/publications/s2012-shading-course/burley/s2012_pbs_disney_brdf_notes_v3.pdf p26
167void SampleAnisoGGXDir(real2 u,
168 real3 V,
169 real3 N,
170 real3 tangentX,
171 real3 tangentY,
172 real roughnessT,
173 real roughnessB,
174 out real3 H,
175 out real3 L)
176{
177 // AnisoGGX NDF sampling
178 H = sqrt(u.x / (1.0 - u.x)) * (roughnessT * cos(TWO_PI * u.y) * tangentX + roughnessB * sin(TWO_PI * u.y) * tangentY) + N;
179 H = normalize(H);
180
181 // Convert sample from half angle to incident angle
182 L = 2.0 * saturate(dot(V, H)) * H - V;
183}
184
185// Adapted from: "Sampling the GGX Distribution of Visible Normals", by E. Heitz
186// http://jcgt.org/published/0007/04/01/paper.pdf
187void SampleAnisoGGXVisibleNormal(float2 u,
188 float3 V,
189 float3x3 localToWorld,
190 float roughnessX,
191 float roughnessY,
192 out float3 localV,
193 out float3 localH,
194 out float VdotH)
195{
196 localV = mul(V, transpose(localToWorld));
197
198 // Construct an orthonormal basis around the stretched view direction
199 float3x3 viewToLocal;
200 viewToLocal[2] = normalize(float3(roughnessX * localV.x, roughnessY * localV.y, localV.z));
201 viewToLocal[0] = (viewToLocal[2].z < 0.9999) ? normalize(cross(float3(0, 0, 1), viewToLocal[2])) : float3(1, 0, 0);
202 viewToLocal[1] = cross(viewToLocal[2], viewToLocal[0]);
203
204 // Compute a sample point with polar coordinates (r, phi)
205 float r = sqrt(u.x);
206 float phi = 2.0 * PI * u.y;
207 float t1 = r * cos(phi);
208 float t2 = r * sin(phi);
209 float s = 0.5 * (1.0 + viewToLocal[2].z);
210 t2 = (1.0 - s) * sqrt(1.0 - t1 * t1) + s * t2;
211
212 // Reproject onto hemisphere
213 localH = t1 * viewToLocal[0] + t2 * viewToLocal[1] + sqrt(max(0.0, 1.0 - t1 * t1 - t2 * t2)) * viewToLocal[2];
214
215 // Transform the normal back to the ellipsoid configuration
216 localH = normalize(float3(roughnessX * localH.x, roughnessY * localH.y, max(0.0, localH.z)));
217
218 VdotH = saturate(dot(localV, localH));
219}
220
221// GGX vsible normal sampling, isotropic variant
222void SampleGGXVisibleNormal(float2 u,
223 float3 V,
224 float3x3 localToWorld,
225 float roughness,
226 out float3 localV,
227 out float3 localH,
228 out float VdotH)
229{
230 SampleAnisoGGXVisibleNormal(u, V, localToWorld, roughness, roughness, localV, localH, VdotH);
231}
232
233// weightOverPdf return the weight (without the diffuseAlbedo term) over pdf. diffuseAlbedo term must be apply by the caller.
234void ImportanceSampleLambert(real2 u,
235 real3x3 localToWorld,
236 out real3 L,
237 out real NdotL,
238 out real weightOverPdf)
239{
240#if 0
241 real3 localL = SampleHemisphereCosine(u.x, u.y);
242
243 NdotL = localL.z;
244
245 L = mul(localL, localToWorld);
246#else
247 real3 N = localToWorld[2];
248
249 L = SampleHemisphereCosine(u.x, u.y, N);
250 NdotL = saturate(dot(N, L));
251#endif
252
253 // Importance sampling weight for each sample
254 // pdf = N.L / PI
255 // weight = fr * (N.L) with fr = diffuseAlbedo / PI
256 // weight over pdf is:
257 // weightOverPdf = (diffuseAlbedo / PI) * (N.L) / (N.L / PI)
258 // weightOverPdf = diffuseAlbedo
259 // diffuseAlbedo is apply outside the function
260
261 weightOverPdf = 1.0;
262}
263
264// weightOverPdf return the weight (without the Fresnel term) over pdf. Fresnel term must be apply by the caller.
265void ImportanceSampleGGX(real2 u,
266 real3 V,
267 real3x3 localToWorld,
268 real roughness,
269 real NdotV,
270 out real3 L,
271 out real VdotH,
272 out real NdotL,
273 out real weightOverPdf)
274{
275 real NdotH;
276 SampleGGXDir(u, V, localToWorld, roughness, L, NdotL, NdotH, VdotH);
277
278 // Importance sampling weight for each sample
279 // pdf = D(H) * (N.H) / (4 * (L.H))
280 // weight = fr * (N.L) with fr = F(H) * G(V, L) * D(H) / (4 * (N.L) * (N.V))
281 // weight over pdf is:
282 // weightOverPdf = F(H) * G(V, L) * (L.H) / ((N.H) * (N.V))
283 // weightOverPdf = F(H) * 4 * (N.L) * V(V, L) * (L.H) / (N.H) with V(V, L) = G(V, L) / (4 * (N.L) * (N.V))
284 // Remind (L.H) == (V.H)
285 // F is apply outside the function
286
287 real Vis = V_SmithJointGGX(NdotL, NdotV, roughness);
288 weightOverPdf = 4.0 * Vis * NdotL * VdotH / NdotH;
289}
290
291// weightOverPdf return the weight (without the Fresnel term) over pdf. Fresnel term must be apply by the caller.
292void ImportanceSampleAnisoGGX(real2 u,
293 real3 V,
294 real3x3 localToWorld,
295 real roughnessT,
296 real roughnessB,
297 real NdotV,
298 out real3 L,
299 out real VdotH,
300 out real NdotL,
301 out real weightOverPdf)
302{
303 real3 tangentX = localToWorld[0];
304 real3 tangentY = localToWorld[1];
305 real3 N = localToWorld[2];
306
307 real3 H;
308 SampleAnisoGGXDir(u, V, N, tangentX, tangentY, roughnessT, roughnessB, H, L);
309
310 real NdotH = saturate(dot(N, H));
311 // Note: since L and V are symmetric around H, LdotH == VdotH
312 VdotH = saturate(dot(V, H));
313 NdotL = saturate(dot(N, L));
314
315 // Importance sampling weight for each sample
316 // pdf = D(H) * (N.H) / (4 * (L.H))
317 // weight = fr * (N.L) with fr = F(H) * G(V, L) * D(H) / (4 * (N.L) * (N.V))
318 // weight over pdf is:
319 // weightOverPdf = F(H) * G(V, L) * (L.H) / ((N.H) * (N.V))
320 // weightOverPdf = F(H) * 4 * (N.L) * V(V, L) * (L.H) / (N.H) with V(V, L) = G(V, L) / (4 * (N.L) * (N.V))
321 // Remind (L.H) == (V.H)
322 // F is apply outside the function
323
324 // For anisotropy we must not saturate these values
325 real TdotV = dot(tangentX, V);
326 real BdotV = dot(tangentY, V);
327 real TdotL = dot(tangentX, L);
328 real BdotL = dot(tangentY, L);
329
330 real Vis = V_SmithJointGGXAniso(TdotV, BdotV, NdotV, TdotL, BdotL, NdotL, roughnessT, roughnessB);
331 weightOverPdf = 4.0 * Vis * NdotL * VdotH / NdotH;
332}
333
334// ----------------------------------------------------------------------------
335// Pre-integration
336// ----------------------------------------------------------------------------
337
338// Ref: Listing 18 in "Moving Frostbite to PBR" + https://knarkowicz.wordpress.com/2014/12/27/analytical-dfg-term-for-ibl/
339real4 IntegrateGGXAndDisneyDiffuseFGD(real NdotV, real roughness, uint sampleCount = 4096)
340{
341 // Note that our LUT covers the full [0, 1] range.
342 // Therefore, we don't really want to clamp NdotV here (else the lerp slope is wrong).
343 // However, if NdotV is 0, the integral is 0, so that's not what we want, either.
344 // Our runtime NdotV bias is quite large, so we use a smaller one here instead.
345 NdotV = max(NdotV, REAL_EPS);
346 real3 V = real3(sqrt(1 - NdotV * NdotV), 0, NdotV);
347 real4 acc = real4(0.0, 0.0, 0.0, 0.0);
348
349 real3x3 localToWorld = k_identity3x3;
350
351 for (uint i = 0; i < sampleCount; ++i)
352 {
353 real2 u = Hammersley2d(i, sampleCount);
354
355 real VdotH;
356 real NdotL;
357 real weightOverPdf;
358
359 real3 L; // Unused
360 ImportanceSampleGGX(u, V, localToWorld, roughness, NdotV,
361 L, VdotH, NdotL, weightOverPdf);
362
363 if (NdotL > 0.0)
364 {
365 // Integral{BSDF * <N,L> dw} =
366 // Integral{(F0 + (1 - F0) * (1 - <V,H>)^5) * (BSDF / F) * <N,L> dw} =
367 // (1 - F0) * Integral{(1 - <V,H>)^5 * (BSDF / F) * <N,L> dw} + F0 * Integral{(BSDF / F) * <N,L> dw}=
368 // (1 - F0) * x + F0 * y = lerp(x, y, F0)
369
370 acc.x += weightOverPdf * pow(1 - VdotH, 5);
371 acc.y += weightOverPdf;
372 }
373
374 // for Disney we still use a Cosine importance sampling, true Disney importance sampling imply a look up table
375 ImportanceSampleLambert(u, localToWorld, L, NdotL, weightOverPdf);
376
377 if (NdotL > 0.0)
378 {
379 real LdotV = dot(L, V);
380 real disneyDiffuse = DisneyDiffuseNoPI(NdotV, NdotL, LdotV, RoughnessToPerceptualRoughness(roughness));
381
382 acc.z += disneyDiffuse * weightOverPdf;
383 }
384 }
385
386 acc /= sampleCount;
387
388 // Remap from the [0.5, 1.5] to the [0, 1] range.
389 acc.z -= 0.5;
390
391 return acc;
392}
393
394uint GetIBLRuntimeFilterSampleCount(uint mipLevel)
395{
396 uint sampleCount = 0;
397
398 switch (mipLevel)
399 {
400 case 1: sampleCount = 21; break;
401 case 2: sampleCount = 34; break;
402#if defined(SHADER_API_MOBILE) || defined(SHADER_API_SWITCH)
403 case 3: sampleCount = 34; break;
404 case 4: sampleCount = 34; break;
405 case 5: sampleCount = 34; break;
406 case 6: sampleCount = 34; break; // UNITY_SPECCUBE_LOD_STEPS
407#else
408 case 3: sampleCount = 55; break;
409 case 4: sampleCount = 89; break;
410 case 5: sampleCount = 89; break;
411 case 6: sampleCount = 89; break; // UNITY_SPECCUBE_LOD_STEPS
412#endif
413 }
414
415 return sampleCount;
416}
417
418// Ref: Listing 19 in "Moving Frostbite to PBR"
419float4 IntegrateLD(TEXTURECUBE_PARAM(tex, sampl),
420 TEXTURE2D(ggxIblSamples),
421 real3 V,
422 real3 N,
423 real roughness,
424 real index, // Current MIP level minus one
425 real invOmegaP,
426 uint sampleCount, // Must be a Fibonacci number
427 bool prefilter,
428 bool usePrecomputedSamples)
429{
430 real3x3 localToWorld = GetLocalFrame(N);
431
432#ifndef USE_KARIS_APPROXIMATION
433 real NdotV = 1; // N == V
434 real partLambdaV = GetSmithJointGGXPartLambdaV(NdotV, roughness);
435#endif
436
437 float3 lightInt = float3(0.0, 0.0, 0.0);
438 float cbsdfInt = 0.0;
439
440 for (uint i = 0; i < sampleCount; ++i)
441 {
442 real3 L;
443 real NdotL, NdotH, LdotH;
444
445 if (usePrecomputedSamples)
446 {
447 // Performance warning: using a texture LUT will generate a vector load,
448 // which increases both the VGPR pressure and the workload of the
449 // texture unit. A better solution here is to load from a Constant, Raw
450 // or Structured buffer, or perhaps even declare all the constants in an
451 // HLSL header to allow the compiler to inline everything.
452 real3 localL = LOAD_TEXTURE2D(ggxIblSamples, uint2(i, index)).xyz;
453
454 L = mul(localL, localToWorld);
455 NdotL = localL.z;
456 LdotH = sqrt(0.5 + 0.5 * NdotL);
457 }
458 else
459 {
460 real2 u = Fibonacci2d(i, sampleCount);
461
462 // Note: if (N == V), all of the microsurface normals are visible.
463 SampleGGXDir(u, V, localToWorld, roughness, L, NdotL, NdotH, LdotH, true);
464
465 if (NdotL <= 0) continue; // Note that some samples will have 0 contribution
466 }
467
468 real mipLevel;
469
470 if (!prefilter) // BRDF importance sampling
471 {
472 mipLevel = 0;
473 }
474 else // Prefiltered BRDF importance sampling
475 {
476 // Use lower MIP-map levels for fetching samples with low probabilities
477 // in order to reduce the variance.
478 // Ref: http://http.developer.nvidia.com/GPUGems3/gpugems3_ch20.html
479 //
480 // - OmegaS: Solid angle associated with the sample
481 // - OmegaP: Solid angle associated with the texel of the cubemap
482
483 real omegaS;
484
485 if (usePrecomputedSamples)
486 {
487 omegaS = LOAD_TEXTURE2D(ggxIblSamples, uint2(i, index)).w;
488 }
489 else
490 {
491 // real PDF = D * NdotH * Jacobian, where Jacobian = 1 / (4 * LdotH).
492 // Since (N == V), NdotH == LdotH.
493 real pdf = 0.25 * D_GGX(NdotH, roughness);
494 // TODO: improve the accuracy of the sample's solid angle fit for GGX.
495 omegaS = rcp(sampleCount) * rcp(pdf);
496 }
497
498 // 'invOmegaP' is precomputed on CPU and provided as a parameter to the function.
499 // real omegaP = FOUR_PI / (6.0 * cubemapWidth * cubemapWidth);
500 const real mipBias = roughness;
501 mipLevel = 0.5 * log2(omegaS * invOmegaP) + mipBias;
502 }
503
504 // TODO: use a Gaussian-like filter to generate the MIP pyramid.
505 real3 val = SAMPLE_TEXTURECUBE_LOD(tex, sampl, L, mipLevel).rgb;
506
507 // The goal of this function is to use Monte-Carlo integration to find
508 // X = Integral{Radiance(L) * CBSDF(L, N, V) dL} / Integral{CBSDF(L, N, V) dL}.
509 // Note: Integral{CBSDF(L, N, V) dL} is given by the FDG texture.
510 // CBSDF = F * D * G * NdotL / (4 * NdotL * NdotV) = F * D * G / (4 * NdotV).
511 // PDF = D * NdotH / (4 * LdotH).
512 // Weight = CBSDF / PDF = F * G * LdotH / (NdotV * NdotH).
513 // Since we perform filtering with the assumption that (V == N),
514 // (LdotH == NdotH) && (NdotV == 1) && (Weight == F * G).
515 // Therefore, after the Monte Carlo expansion of the integrals,
516 // X = Sum(Radiance(L) * Weight) / Sum(Weight) = Sum(Radiance(L) * F * G) / Sum(F * G).
517
518 #ifndef USE_KARIS_APPROXIMATION
519 // The choice of the Fresnel factor does not appear to affect the result.
520 real F = 1; // F_Schlick(F0, LdotH);
521 real G = V_SmithJointGGX(NdotL, NdotV, roughness, partLambdaV) * NdotL * NdotV; // 4 cancels out
522
523 lightInt += F * G * val;
524 cbsdfInt += F * G;
525 #else
526 // Use the approximation from "Real Shading in Unreal Engine 4": Weight ~ NdotL.
527 lightInt += NdotL * val;
528 cbsdfInt += NdotL;
529 #endif
530 }
531
532 return float4(lightInt / cbsdfInt, 1.0);
533}
534
535real4 IntegrateLDCharlie(TEXTURECUBE_PARAM(tex, sampl),
536 real3 N,
537 real roughness,
538 uint sampleCount,
539 real invFaceCenterTexelSolidAngle)
540{
541 // ensure proper values
542 roughness = max(roughness, 0.001f);
543 sampleCount = max(1, sampleCount);
544
545 // filtered uniform sampling of the hemisphere
546 real3x3 localToWorld = GetLocalFrame(N);
547 real3 totalLight = real3(0.0, 0.0, 0.0);
548 real totalWeight = 0.0;
549 real rcpNumSamples = rcp(sampleCount);
550 real pdf = 1 / (2.0f * PI);
551 real lodBias = roughness;
552 real lodBase = 0.5f * log2((rcpNumSamples * 1.0f / pdf) * invFaceCenterTexelSolidAngle) + lodBias;
553 for (uint i = 0; i < sampleCount; ++i)
554 {
555 // generate sample on the normal oriented hemisphere (uniform sampling)
556 real3 localL = SampleConeStrata(i, rcpNumSamples, 0.0f);
557 real NdotL = localL.z;
558 real3 L = mul(localL, localToWorld);
559
560 // evaluate BRDF for the sample (assume V=N)
561 real NdotV = 1.0;
562 real LdotV, NdotH, LdotH, invLenLV;
563 GetBSDFAngle(N, L, NdotL, NdotV, LdotV, NdotH, LdotH, invLenLV);
564 real D = D_Charlie(NdotH, roughness);
565
566 // calculate texture LOD: 0.5*log2(omegaS/omegaP) as descriped in GPU Gems 3 "GPU-Based Importance Sampling" chapter 20.4:
567 // https://developer.nvidia.com/gpugems/gpugems3/part-iii-rendering/chapter-20-gpu-based-importance-sampling
568 // omegaS = solid angle of the sample (i.e. 2pi/sampleCount for uniform hemisphere sampling)
569 // omegaP = solid angle of the texel in the sample direction. This is calculated by multiplying solid angle
570 // of the face center texel with texel cos(theta), where theta is angle between sample direction
571 // and center of the face, to account diminishing texel solid angles towards the edges of the cube.
572 real3 cubeCoord = L / max(abs(L.x), max(abs(L.y), abs(L.z))); // project sample direction to the cube face
573 real invDu2 = dot(cubeCoord, cubeCoord); // invDu2=1/cos^2(theta) of the sample texel
574 real lod = 0.5f * 0.5f * log2(invDu2) + lodBase; // extra 0.5f for sqrt(invDu2)=1/cos(theta)
575 real3 val = SAMPLE_TEXTURECUBE_LOD(tex, sampl, L, lod).rgb;
576
577 // accumulate lighting & weights
578 real w = D * NdotL;
579 totalLight += val * w;
580 totalWeight += w;
581 }
582
583 return real4(totalLight / totalWeight, 1.0);
584}
585
586// Searches the row 'j' containing 'n' elements of 'haystack' and
587// returns the index of the first element greater or equal to 'needle'.
588uint BinarySearchRow(uint j, real needle, TEXTURE2D(haystack), uint n)
589{
590 uint i = n - 1;
591 real v = LOAD_TEXTURE2D(haystack, uint2(i, j)).r;
592
593 if (needle < v)
594 {
595 i = 0;
596
597 for (uint b = 1U << firstbithigh(n - 1); b != 0; b >>= 1)
598 {
599 uint p = i | b;
600 v = LOAD_TEXTURE2D(haystack, uint2(p, j)).r;
601 if (v <= needle) { i = p; } // Move to the right.
602 }
603 }
604
605 return i;
606}
607
608real4 IntegrateLD_MIS(TEXTURECUBE_PARAM(envMap, sampler_envMap),
609 TEXTURE2D(marginalRowDensities),
610 TEXTURE2D(conditionalDensities),
611 real3 V,
612 real3 N,
613 real roughness,
614 real invOmegaP,
615 uint width,
616 uint height,
617 uint sampleCount,
618 bool prefilter)
619{
620 real3x3 localToWorld = GetLocalFrame(N);
621
622 real3 lightInt = real3(0.0, 0.0, 0.0);
623 real cbsdfInt = 0.0;
624
625/*
626 // Dedicate 50% of samples to light sampling at 1.0 roughness.
627 // Only perform BSDF sampling when roughness is below 0.5.
628 const int lightSampleCount = lerp(0, sampleCount / 2, saturate(2.0 * roughness - 1.0));
629 const int bsdfSampleCount = sampleCount - lightSampleCount;
630*/
631
632 // The value of the integral of intensity values of the environment map (as a 2D step function).
633 real envMapInt2dStep = LOAD_TEXTURE2D(marginalRowDensities, uint2(height, 0)).r;
634 // Since we are using equiareal mapping, we need to divide by the area of the sphere.
635 real envMapIntSphere = envMapInt2dStep * INV_FOUR_PI;
636
637 // Perform light importance sampling.
638 for (uint i = 0; i < sampleCount; i++)
639 {
640 real2 s = Hammersley2d(i, sampleCount);
641
642 // Sample a row from the marginal distribution.
643 uint y = BinarySearchRow(0, s.x, marginalRowDensities, height - 1);
644
645 // Sample a column from the conditional distribution.
646 uint x = BinarySearchRow(y, s.y, conditionalDensities, width - 1);
647
648 // Compute the coordinates of the sample.
649 // Note: we take the sample in between two texels, and also apply the half-texel offset.
650 // We could compute fractional coordinates at the cost of 4 extra texel samples.
651 real u = saturate((real)x / width + 1.0 / width);
652 real v = saturate((real)y / height + 1.0 / height);
653 real3 L = ConvertEquiarealToCubemap(u, v);
654
655 real NdotL = saturate(dot(N, L));
656
657 if (NdotL > 0.0)
658 {
659 real3 val = SAMPLE_TEXTURECUBE_LOD(envMap, sampler_envMap, L, 0).rgb;
660 real pdf = (val.r + val.g + val.b) / envMapIntSphere;
661
662 if (pdf > 0.0)
663 {
664 // (N == V) && (acos(VdotL) == 2 * acos(NdotH)).
665 real NdotH = sqrt(NdotL * 0.5 + 0.5);
666
667 // *********************************************************************************
668 // Our goal is to use Monte-Carlo integration with importance sampling to evaluate
669 // X(V) = Integral{Radiance(L) * CBSDF(L, N, V) dL} / Integral{CBSDF(L, N, V) dL}.
670 // CBSDF = F * D * G * NdotL / (4 * NdotL * NdotV) = F * D * G / (4 * NdotV).
671 // Weight = CBSDF / PDF.
672 // We use two approximations of Brian Karis from "Real Shading in Unreal Engine 4":
673 // (F * G ~ NdotL) && (NdotV == 1).
674 // Weight = D * NdotL / (4 * PDF).
675 // *********************************************************************************
676
677 real weight = D_GGX(NdotH, roughness) * NdotL / (4.0 * pdf);
678
679 lightInt += weight * val;
680 cbsdfInt += weight;
681 }
682 }
683 }
684
685 // Prevent NaNs arising from the division of 0 by 0.
686 cbsdfInt = max(cbsdfInt, REAL_EPS);
687
688 return real4(lightInt / cbsdfInt, 1.0);
689}
690
691// Little helper to share code between sphere and box reflection probe.
692// This function will fade the mask of a reflection volume based on normal orientation compare to direction define by the center of the reflection volume.
693float InfluenceFadeNormalWeight(float3 normal, float3 centerToPos)
694{
695 // Start weight from 0.6f (1 fully transparent) to 0.2f (fully opaque).
696 return saturate((-1.0f / 0.4f) * dot(normal, centerToPos) + (0.6f / 0.4f));
697}
698
699#if SHADER_API_MOBILE || SHADER_API_GLES3 || SHADER_API_SWITCH
700#pragma warning (enable : 3205) // conversion of larger type to smaller
701#endif
702
703#endif // UNITY_IMAGE_BASED_LIGHTING_HLSL_INCLUDED