#ifndef FILAMENT_LIGHT_INDIRECT #define FILAMENT_LIGHT_INDIRECT #include "FilamentCommonOcclusion.cginc" #include "FilamentCommonGraphics.cginc" #include "FilamentBRDF.cginc" #include "UnityImageBasedLightingMinimal.cginc" #include "UnityStandardUtils.cginc" #include "UnityLightingCommon.cginc" #include "SharedFilteringLib.hlsl" #include "SharedSHLib.hlsl" #include "SharedLightmapLib.hlsl" #include "FilamentLightLTCGI.cginc" #include "FilamentLightVRCLV.cginc" #include "FilamentLightMirror.cginc" //------------------------------------------------------------------------------ // Image based lighting configuration //------------------------------------------------------------------------------ // Spherical harmonics sampling algorithm // Unity's default; basic SH sampling, with L2 SH enabled #define SPHERICAL_HARMONICS_DEFAULT 0 // Geometrics' deringing lightprobe sampling, using L1 SH #define SPHERICAL_HARMONICS_GEOMETRICS 1 // Activision's Quadratic Zonal Harmonics, using L1 SH #define SPHERICAL_HARMONICS_ZH3 2 #define SPHERICAL_HARMONICS SPHERICAL_HARMONICS_ZH3 // VRC Light Volumes-specific #define SPHERICAL_HARMONICS_VRCLV SPHERICAL_HARMONICS_GEOMETRICS // Whether to use non-linear SH sampling on Bakery lightmap modes. // If ZH3 sampling is used, it will be used here. This increases the quality of the directional // lighting on lightmapped surfaces, in exchange for a performance cost. #define BAKERY_SHNONLINEAR 0 // IBL integration algorithm #define IBL_INTEGRATION_PREFILTERED_CUBEMAP 0 #define IBL_INTEGRATION_IMPORTANCE_SAMPLING 1 // Not supported! #define IBL_INTEGRATION IBL_INTEGRATION_PREFILTERED_CUBEMAP #define IBL_INTEGRATION_IMPORTANCE_SAMPLING_COUNT 64 // Refraction defines #define REFRACTION_TYPE_SOLID 0 #define REFRACTION_TYPE_THIN 1 #ifndef REFRACTION_TYPE #define REFRACTION_TYPE REFRACTION_TYPE_SOLID #endif #define REFRACTION_MODE_CUBEMAP 0 #define REFRACTION_MODE_SCREEN 1 #ifndef REFRACTION_MODE #define REFRACTION_MODE REFRACTION_MODE_CUBEMAP #endif //------------------------------------------------------------------------------ // IBL prefiltered DFG term implementations //------------------------------------------------------------------------------ half3 PrefilteredDFG_LUT(half lod, half NoV) { #if defined(USE_DFG_LUT) // coord = sqrt(linear_roughness), which is the mapping used by cmgen. return UNITY_SAMPLE_TEX2D(_DFG, half2(NoV, lod)); #else // Texture not available return float3(1.0, 0.0, 0.0); #endif } //------------------------------------------------------------------------------ // IBL environment BRDF dispatch //------------------------------------------------------------------------------ half3 prefilteredDFG(half perceptualRoughness, half NoV) { #if defined(USE_DFG_LUT) // PrefilteredDFG_LUT() takes a LOD, which is sqrt(roughness) = perceptualRoughness return PrefilteredDFG_LUT(perceptualRoughness, NoV); #else #if 0 // Karis' approximation based on Lazarov's const half4 c0 = half4(-1.0, -0.0275, -0.572, 0.022); const half4 c1 = half4( 1.0, 0.0425, 1.040, -0.040); half4 r = perceptualRoughness * c0 + c1; half a004 = min(r.x * r.x, exp2(-9.28 * NoV)) * r.x + r.y; return (half3(half2(-1.04, 1.04) * a004 + r.zw, 0.0)); #endif #if 0 // Zioma's approximation based on Karis return half3(half2(1.0, pow(1.0 - max(perceptualRoughness, NoV), 3.0)), 0.0); #endif return half3(0, 1, 1); #endif } //------------------------------------------------------------------------------ // IBL irradiance implementations //------------------------------------------------------------------------------ half3 Irradiance_SphericalHarmonics(const half3 n, const bool useL2) { // Uses Unity's functions for reading SH. half3 finalSH = half3(0,0,0); #if (SPHERICAL_HARMONICS == SPHERICAL_HARMONICS_DEFAULT) finalSH = SHEvalLinearL0L1(half4(n, 1.0)); #endif #if (SPHERICAL_HARMONICS == SPHERICAL_HARMONICS_GEOMETRICS) half3 L0 = half3(unity_SHAr.w, unity_SHAg.w, unity_SHAb.w); half3 L0L2 = half3(unity_SHBr.z, unity_SHBg.z, unity_SHBb.z) / 3.0; L0 = (useL2) ? L0+L0L2 : L0-L0L2; finalSH.r = shEvaluateDiffuseL1Geomerics_local(L0.r, unity_SHAr.xyz, n); finalSH.g = shEvaluateDiffuseL1Geomerics_local(L0.g, unity_SHAg.xyz, n); finalSH.b = shEvaluateDiffuseL1Geomerics_local(L0.b, unity_SHAb.xyz, n); // Quadratic polynomials #endif #if (SPHERICAL_HARMONICS == SPHERICAL_HARMONICS_ZH3) finalSH = SHEvalLinearL0L1_ZH3Hallucinate(half4(n, 1.0)); #endif // L2 contribution. // Note that if UNITY_SAMPLE_FULL_SH_PER_PIXEL is not set, L2 will not be added here! #if (SPHERICAL_HARMONICS == SPHERICAL_HARMONICS_DEFAULT) if (useL2) finalSH += SHEvalLinearL2(half4(n, 1.0)); #endif return finalSH; } half3 Irradiance_SphericalHarmonics(const float3 n) { // Assume L2 is wanted return Irradiance_SphericalHarmonics(n, true); } #if UNITY_LIGHT_PROBE_PROXY_VOLUME // normal should be normalized, w=1.0 half3 Irradiance_SampleProbeVolume (half4 normal, float3 worldPos) { const float transformToLocal = unity_ProbeVolumeParams.y; const float texelSizeX = unity_ProbeVolumeParams.z; //The SH coefficients textures and probe occlusion are packed into 1 atlas. //------------------------- //| ShR | ShG | ShB | Occ | //------------------------- float3 position = (transformToLocal == 1.0f) ? mul(unity_ProbeVolumeWorldToObject, float4(worldPos, 1.0)).xyz : worldPos; float3 texCoord = (position - unity_ProbeVolumeMin.xyz) * unity_ProbeVolumeSizeInv.xyz; texCoord.x = texCoord.x * 0.25f; // We need to compute proper X coordinate to sample. // Clamp the coordinate otherwize we'll have leaking between RGB coefficients float texCoordX = clamp(texCoord.x, 0.5f * texelSizeX, 0.25f - 0.5f * texelSizeX); // sampler state comes from SHr (all SH textures share the same sampler) texCoord.x = texCoordX; half4 SHAr = UNITY_SAMPLE_TEX3D_SAMPLER(unity_ProbeVolumeSH, unity_ProbeVolumeSH, texCoord); texCoord.x = texCoordX + 0.25f; half4 SHAg = UNITY_SAMPLE_TEX3D_SAMPLER(unity_ProbeVolumeSH, unity_ProbeVolumeSH, texCoord); texCoord.x = texCoordX + 0.5f; half4 SHAb = UNITY_SAMPLE_TEX3D_SAMPLER(unity_ProbeVolumeSH, unity_ProbeVolumeSH, texCoord); // Linear + constant polynomial terms half3 x1; #if (SPHERICAL_HARMONICS == SPHERICAL_HARMONICS_DEFAULT) x1.r = dot(SHAr, normal); x1.g = dot(SHAg, normal); x1.b = dot(SHAb, normal); #endif #if (SPHERICAL_HARMONICS == SPHERICAL_HARMONICS_GEOMETRICS) x1.r = shEvaluateDiffuseL1Geomerics_local(SHAr.w, SHAr.rgb, normal); x1.g = shEvaluateDiffuseL1Geomerics_local(SHAg.w, SHAg.rgb, normal); x1.b = shEvaluateDiffuseL1Geomerics_local(SHAb.w, SHAb.rgb, normal); #endif #if (SPHERICAL_HARMONICS == SPHERICAL_HARMONICS_ZH3) x1.r = SHEvalLinearL0L1_ZH3Hallucinate(float4(SHAr.w, SHAr.rgb), normal); x1.g = SHEvalLinearL0L1_ZH3Hallucinate(float4(SHAg.w, SHAg.rgb), normal); x1.b = SHEvalLinearL0L1_ZH3Hallucinate(float4(SHAb.w, SHAb.rgb), normal); #endif return x1; } #endif #if defined(_VRCLV) half3 Irradiance_SampleVRCLightVolume(half3 normal, float3 worldPos, out Light derivedLight) { derivedLight = (Light)0; // Bias sampling by surface normal to avoid artifacts // Fetch Spherical Harmonics (SH) components from the VRC Light Volume float3 L0, L1r, L1g, L1b; LightVolumeSH(worldPos, L0, L1r, L1g, L1b, normal * getLightVolumeSurfaceBias()); // Compute irradiance using the SH components half3 irradiance = 0.0; #if (SPHERICAL_HARMONICS_VRCLV == SPHERICAL_HARMONICS_DEFAULT) irradiance.r = dot(L1r, normal.xyz) + L0.r; irradiance.g = dot(L1g, normal.xyz) + L0.g; irradiance.b = dot(L1b, normal.xyz) + L0.b; #endif #if (SPHERICAL_HARMONICS_VRCLV == SPHERICAL_HARMONICS_GEOMETRICS) irradiance.r = shEvaluateDiffuseL1Geomerics_local(L0.r, L1r, normal.xyz); irradiance.g = shEvaluateDiffuseL1Geomerics_local(L0.g, L1g, normal.xyz); irradiance.b = shEvaluateDiffuseL1Geomerics_local(L0.b, L1b, normal.xyz); #endif #if (SPHERICAL_HARMONICS_VRCLV == SPHERICAL_HARMONICS_ZH3) irradiance = SHEvalLinearL0L1_ZH3Hallucinate(normal.xyz, L0, L1r, L1g, L1b); #endif #if defined(LIGHTMAP_SPECULAR) half3 nL1x; half3 nL1y; half3 nL1z; nL1x = half3(L1r[0], L1g[0], L1b[0]); nL1y = half3(L1r[1], L1g[1], L1b[1]); nL1z = half3(L1r[2], L1g[2], L1b[2]); half3 dominantDir = half3(luminance(nL1x), luminance(nL1y), luminance(nL1z)); // Determine the light's direction and directionality half L0_lum = max(FLT_EPS, luminance(L0)); half L1_mag = length(dominantDir); half directionality = saturate(L1_mag / L0_lum); derivedLight.l = dominantDir / L1_mag; half3 directionalColor = half3(dot(L1r, derivedLight.l), dot(L1g, derivedLight.l), dot(L1b, derivedLight.l)); half3 Li = L0 + max(0, directionalColor); derivedLight.colorIntensity = half4(Li, 1.0); derivedLight.attenuation = directionality; derivedLight.NoL = saturate(dot(normal, derivedLight.l)); #endif return irradiance; } half3 Irradiance_SampleVRCLightVolumeAdditive(half3 normal, float3 worldPos, out Light derivedLight) { derivedLight = (Light)0; // Fetch Spherical Harmonics (SH) components from the VRC Light Volume half3 L0, L1r, L1g, L1b; LightVolumeAdditiveSH(worldPos, L0, L1r, L1g, L1b, normal * getLightVolumeSurfaceBias()); // Compute irradiance using the SH components half3 irradiance = 0.0; // Doesn't support non-linear evaluation, so just evaluate normally. irradiance.r = dot(L1r, normal.xyz) + L0.r; irradiance.g = dot(L1g, normal.xyz) + L0.g; irradiance.b = dot(L1b, normal.xyz) + L0.b; // Add derived light to existing derived light #if defined(LIGHTMAP_SPECULAR) half3 nL1x; half3 nL1y; half3 nL1z; nL1x = half3(L1r[0], L1g[0], L1b[0]); nL1y = half3(L1r[1], L1g[1], L1b[1]); nL1z = half3(L1r[2], L1g[2], L1b[2]); half3 dominantDir = half3(luminance(nL1x), luminance(nL1y), luminance(nL1z)); // Determine the light's direction and directionality half L0_lum = max(FLT_EPS, luminance(L0)); half L1_mag = length(dominantDir); half directionality = saturate(L1_mag / L0_lum); derivedLight.l = dominantDir / L1_mag; half3 directionalColor = half3(dot(L1r, derivedLight.l), dot(L1g, derivedLight.l), dot(L1b, derivedLight.l)); half3 Li = L0 + max(0, directionalColor); derivedLight.colorIntensity = half4(Li, 1.0); derivedLight.attenuation = directionality; derivedLight.NoL = saturate(dot(normal, derivedLight.l)); #endif return irradiance; } #endif half3 Irradiance_SphericalHarmonicsUnity (half3 normal, half3 ambient, float3 worldPos, out Light derivedLight) { half3 ambient_contrib = 0.0; derivedLight = (Light)0; // Gather VRC Light Volumes data, if present. // This replaces the result of light probes. #if defined(_VRCLV) #if UNITY_LIGHT_PROBE_PROXY_VOLUME // I feel like this is an insane edge case if (unity_ProbeVolumeParams.x == 1.0) ambient_contrib = Irradiance_SampleProbeVolume(half4(normal, 1.0), worldPos); else ambient_contrib = Irradiance_SampleVRCLightVolume(normal, worldPos, derivedLight); #else ambient_contrib = Irradiance_SampleVRCLightVolume(normal, worldPos, derivedLight); #endif ambient += max(half3(0, 0, 0), ambient_contrib); #ifdef UNITY_COLORSPACE_GAMMA ambient = LinearToGammaSpace (ambient); #endif return ambient; #else #if UNITY_SAMPLE_FULL_SH_PER_PIXEL // Completely per-pixel #if UNITY_LIGHT_PROBE_PROXY_VOLUME if (unity_ProbeVolumeParams.x == 1.0) ambient_contrib = Irradiance_SampleProbeVolume(half4(normal, 1.0), worldPos); else ambient_contrib = Irradiance_SphericalHarmonics(normal, true); #else ambient_contrib = Irradiance_SphericalHarmonics(normal, true); #endif ambient += max(half3(0, 0, 0), ambient_contrib); #ifdef UNITY_COLORSPACE_GAMMA ambient = LinearToGammaSpace(ambient); #endif #elif (SHADER_TARGET < 30) || UNITY_STANDARD_SIMPLE // Completely per-vertex // nothing to do here. Gamma conversion on ambient from SH takes place in the vertex shader, see ShadeSHPerVertex. #else // L2 per-vertex, L0..L1 & gamma-correction per-pixel // Ambient in this case is expected to be always Linear, see ShadeSHPerVertex() #if UNITY_LIGHT_PROBE_PROXY_VOLUME if (unity_ProbeVolumeParams.x == 1.0) ambient_contrib = Irradiance_SampleProbeVolume (half4(normal, 1.0), worldPos); else ambient_contrib = Irradiance_SphericalHarmonics(normal, false); #else ambient_contrib = Irradiance_SphericalHarmonics(normal, false); #endif ambient = max(half3(0, 0, 0), ambient+ambient_contrib); // include L2 contribution in vertex shader before clamp. #ifdef UNITY_COLORSPACE_GAMMA ambient = LinearToGammaSpace (ambient); #endif #endif return ambient; #endif } /* half3 Irradiance_RoughnessOne(const half3 n) { // note: lod used is always integer, hopefully the hardware skips tri-linear filtering return decodeDataForIBL(textureLod(light_iblSpecular, n, frameUniforms.iblRoughnessOneLevel)); } */ half IrradianceToExposureOcclusion(half3 irradiance) { return saturate(length(irradiance + FLT_EPS) * getExposureOcclusionBias()); } // Return light probes or lightmap. float3 UnityGI_Irradiance(ShadingParams shading, float3 tangentNormal, out half occlusion, out Light derivedLight) { float3 irradiance = shading.ambient; // In order for the exposure occlusion to handle mixed lightmaps and etc well, we accumulate // irradiance seperately, and calculate the exposure occlusion from that to avoid having directionality influence it. float3 irradianceForAO; occlusion = 1.0; derivedLight = (Light)0; #if UNITY_SHOULD_SAMPLE_SH irradiance += Irradiance_SphericalHarmonicsUnity(shading.normal, shading.ambient, shading.position, derivedLight); #endif irradianceForAO = irradiance; // Should be stripped out at compile time if vertex LM mode is disabled. if (getIsBakeryVertexMode() == false) { #if defined(LIGHTMAP_ON) // Baked lightmaps half4 bakedColorTex = SampleLightmapBicubic(shading.lightmapUV.xy); half3 bakedColor = DecodeLightmap(bakedColorTex); #ifdef DIRLIGHTMAP_COMBINED fixed4 bakedDirTex = SampleLightmapDirBicubic (shading.lightmapUV.xy); // Bakery's MonoSH mode replaces the regular directional lightmap #if defined(_BAKERY_MONOSH) irradiance = DecodeMonoSHLightmap (bakedColor, bakedDirTex, shading.normal, derivedLight); irradianceForAO = irradiance; #if defined(LIGHTMAP_SHADOW_MIXING) && !defined(SHADOWS_SHADOWMASK) && defined(SHADOWS_SCREEN) irradiance = SubtractMainLightWithRealtimeAttenuationFromLightmap (irradiance, shading.attenuation, bakedColorTex, shading.normal); #endif #else irradiance = DecodeDirectionalLightmap (bakedColor, bakedDirTex, shading.normal); irradianceForAO = irradiance; #if defined(LIGHTMAP_SHADOW_MIXING) && !defined(SHADOWS_SHADOWMASK) && defined(SHADOWS_SCREEN) irradiance = SubtractMainLightWithRealtimeAttenuationFromLightmap (irradiance, shading.attenuation, bakedColorTex, shading.normal); #endif #if defined(LIGHTMAP_SPECULAR) irradiance = DecodeDirectionalLightmapSpecular(bakedColor, bakedDirTex, shading.normal, false, 0, derivedLight); #endif #endif #else // not directional lightmap #if defined(USING_BAKERY) #if defined(_BAKERY_RNM) // bakery rnm mode irradiance = DecodeRNMLightmap(bakedColor, shading.lightmapUV.xy, tangentNormal, shading.tangentToWorld, derivedLight); #endif #if defined(_BAKERY_SH) // bakery sh mode irradiance = DecodeSHLightmap(bakedColor, shading.lightmapUV.xy, shading.normal, derivedLight); #endif irradianceForAO = irradiance; #if defined(LIGHTMAP_SHADOW_MIXING) && !defined(SHADOWS_SHADOWMASK) && defined(SHADOWS_SCREEN) irradiance = SubtractMainLightWithRealtimeAttenuationFromLightmap(irradiance, shading.attenuation, bakedColorTex, shading.normal); #endif #else irradiance += bakedColor; irradianceForAO = irradiance; #if defined(LIGHTMAP_SHADOW_MIXING) && !defined(SHADOWS_SHADOWMASK) && defined(SHADOWS_SCREEN) irradiance = SubtractMainLightWithRealtimeAttenuationFromLightmap(irradiance, shading.attenuation, bakedColorTex, shading.normal); #endif #endif #endif #endif } #if defined(USING_BAKERY_VERTEXLM) if (getIsBakeryVertexMode() == true) { // Lightmap colour is already stored in shading.ambient. // If directionality is on, then ambientDir contains directionality. // If SH is on, then ambientSH[3] contains the SH data. half4 bakedColorTex = float4(shading.ambient, 1.0); #if defined(USING_BAKERY_VERTEXLMSH) irradiance = DecodeSHLightmapVertex(shading.ambient, shading.ambientSH, shading.normal, derivedLight); irradianceForAO = irradiance; #if defined(LIGHTMAP_SHADOW_MIXING) && !defined(SHADOWS_SHADOWMASK) && defined(SHADOWS_SCREEN) irradiance = SubtractMainLightWithRealtimeAttenuationFromLightmap (irradiance, shading.attenuation, bakedColorTex, shading.normal); #endif #else #if defined(USING_BAKERY_VERTEXLMDIR) #if defined(_BAKERY_MONOSH) irradiance = DecodeMonoSHLightmap (shading.ambient, shading.ambientDir, shading.normal, derivedLight, false); irradianceForAO = irradiance; #if defined(LIGHTMAP_SHADOW_MIXING) && !defined(SHADOWS_SHADOWMASK) && defined(SHADOWS_SCREEN) irradiance = SubtractMainLightWithRealtimeAttenuationFromLightmap (irradiance, shading.attenuation, bakedColorTex, shading.normal); #endif #else irradiance = DecodeDirectionalLightmap (shading.ambient, shading.ambientDir, shading.normal); irradianceForAO = irradiance; #if defined(LIGHTMAP_SHADOW_MIXING) && !defined(SHADOWS_SHADOWMASK) && defined(SHADOWS_SCREEN) irradiance = SubtractMainLightWithRealtimeAttenuationFromLightmap (irradiance, shading.attenuation, bakedColorTex, shading.normal); #endif #if defined(LIGHTMAP_SPECULAR) irradiance = DecodeDirectionalLightmapSpecular(shading.ambient, shading.ambientDir, shading.normal, false, 0, derivedLight); #endif #endif #else // No directionality, just light colour. // Irradiance and IrradianceForAO already contain the irradiance, so just handle subtractive lighting. #if defined(LIGHTMAP_SHADOW_MIXING) && !defined(SHADOWS_SHADOWMASK) && defined(SHADOWS_SCREEN) irradiance = SubtractMainLightWithRealtimeAttenuationFromLightmap(irradiance, shading.attenuation, bakedColorTex, shading.normal); #endif #endif #endif } #endif #if defined(DYNAMICLIGHTMAP_ON) // Dynamic lightmaps fixed4 realtimeColorTex = SampleDynamicLightmapBicubic(shading.lightmapUV.zw); half3 realtimeColor = DecodeRealtimeLightmap (realtimeColorTex); irradianceForAO += realtimeColor; #ifdef DIRLIGHTMAP_COMBINED half4 realtimeDirTex = SampleDynamicLightmapDirBicubic(shading.lightmapUV.zw); irradiance += DecodeDirectionalLightmap (realtimeColor, realtimeDirTex, shading.normal); #else irradiance += realtimeColor; #endif #endif occlusion = IrradianceToExposureOcclusion(irradianceForAO); return irradiance; } //------------------------------------------------------------------------------ // IBL irradiance dispatch //------------------------------------------------------------------------------ half3 get_diffuseIrradiance_notUsed(const half3 n) { return Irradiance_SphericalHarmonics(n); } //------------------------------------------------------------------------------ // IBL specular //------------------------------------------------------------------------------ UnityGIInput InitialiseUnityGIInput(const ShadingParams shading, const PixelParams pixel) { UnityGIInput d; d.worldPos = shading.position; d.worldViewDir = -shading.view; d.probeHDR[0] = unity_SpecCube0_HDR; d.probeHDR[1] = unity_SpecCube1_HDR; #if defined(UNITY_SPECCUBE_BLENDING) || defined(UNITY_SPECCUBE_BOX_PROJECTION) d.boxMin[0] = unity_SpecCube0_BoxMin; // .w holds lerp value for blending #endif #ifdef UNITY_SPECCUBE_BOX_PROJECTION d.boxMax[0] = unity_SpecCube0_BoxMax; d.probePosition[0] = unity_SpecCube0_ProbePosition; d.boxMax[1] = unity_SpecCube1_BoxMax; d.boxMin[1] = unity_SpecCube1_BoxMin; d.probePosition[1] = unity_SpecCube1_ProbePosition; #endif return d; } half perceptualRoughnessToLod(half perceptualRoughness) { const half iblRoughnessOneLevel = 1.0/UNITY_SPECCUBE_LOD_STEPS; // The mapping below is a quadratic fit for log2(perceptualRoughness)+iblRoughnessOneLevel when // iblRoughnessOneLevel is 4. We found empirically that this mapping works very well for // a 256 cubemap with 5 levels used. But also scales well for other iblRoughnessOneLevel values. //return iblRoughnessOneLevel * perceptualRoughness * (2.0 - perceptualRoughness); // Unity's remapping (UNITY_SPECCUBE_LOD_STEPS is 6, not 4) return iblRoughnessOneLevel * perceptualRoughness * (1.7 - 0.7 * perceptualRoughness); } half3 prefilteredRadiance(const half3 r, half perceptualRoughness) { half lod = perceptualRoughnessToLod(perceptualRoughness); return DecodeHDR(UNITY_SAMPLE_TEXCUBE_LOD(unity_SpecCube0, r, lod), unity_SpecCube0_HDR); } half3 prefilteredRadiance(const half3 r, half roughness, half offset) { const half iblRoughnessOneLevel = 1.0/UNITY_SPECCUBE_LOD_STEPS; half lod = iblRoughnessOneLevel * roughness; return DecodeHDR(UNITY_SAMPLE_TEXCUBE_LOD(unity_SpecCube0, r, lod + offset), unity_SpecCube0_HDR); } half3 Unity_GlossyEnvironment_local (UNITY_ARGS_TEXCUBE(tex), half4 hdr, Unity_GlossyEnvironmentData glossIn) { half perceptualRoughness = glossIn.roughness /* perceptualRoughness */ ; // Workaround for issue where objects are blurrier than they should be // due to specular AA. #if defined(GEOMETRIC_SPECULAR_AA) half roughnessAdjustment = 1-perceptualRoughness; roughnessAdjustment = MIN_PERCEPTUAL_ROUGHNESS * roughnessAdjustment * roughnessAdjustment; perceptualRoughness = perceptualRoughness - roughnessAdjustment; #endif // Unity derivation perceptualRoughness = perceptualRoughness*(1.7 - 0.7 * perceptualRoughness); // Filament derivation // perceptualRoughness = perceptualRoughness * (2.0 - perceptualRoughness); half mip = perceptualRoughnessToMipmapLevel(perceptualRoughness); half3 R = glossIn.reflUVW; half4 rgbm = UNITY_SAMPLE_TEXCUBE_LOD(tex, R, mip); return DecodeHDR(rgbm, hdr); } // Workaround: Construct the correct Unity variables and get the correct Unity spec values inline half3 UnityGI_prefilteredRadiance(const UnityGIInput data, const half perceptualRoughness, const float3 r) { half3 specular; Unity_GlossyEnvironmentData glossIn = (Unity_GlossyEnvironmentData)0; glossIn.roughness = perceptualRoughness; glossIn.reflUVW = r; #ifdef UNITY_SPECCUBE_BOX_PROJECTION // we will tweak reflUVW in glossIn directly (as we pass it to Unity_GlossyEnvironment twice for probe0 and probe1), so keep original to pass into BoxProjectedCubemapDirection half3 originalReflUVW = glossIn.reflUVW; glossIn.reflUVW = BoxProjectedCubemapDirection (originalReflUVW, data.worldPos, data.probePosition[0], data.boxMin[0], data.boxMax[0]); #endif #ifdef _GLOSSYREFLECTIONS_OFF specular = unity_IndirectSpecColor.rgb; #else half3 env0 = Unity_GlossyEnvironment_local (UNITY_PASS_TEXCUBE(unity_SpecCube0), data.probeHDR[0], glossIn); #ifdef UNITY_SPECCUBE_BLENDING const float kBlendFactor = 0.99999; float blendLerp = data.boxMin[0].w; UNITY_BRANCH if (blendLerp < kBlendFactor) { #ifdef UNITY_SPECCUBE_BOX_PROJECTION glossIn.reflUVW = BoxProjectedCubemapDirection (originalReflUVW, data.worldPos, data.probePosition[1], data.boxMin[1], data.boxMax[1]); #endif half3 env1 = Unity_GlossyEnvironment_local (UNITY_PASS_TEXCUBE_SAMPLER(unity_SpecCube1,unity_SpecCube0), data.probeHDR[1], glossIn); specular = lerp(env1, env0, blendLerp); } else { specular = env0; } #else specular = env0; #endif #endif return specular; } half3 getSpecularDominantDirection(const half3 n, const half3 r, half roughness) { return lerp(r, n, roughness * roughness); } half3 specularDFG(const PixelParams pixel) { #if defined(SHADING_MODEL_CLOTH) return pixel.f0 * pixel.dfg.z; #else return lerp(pixel.dfg.xxx, pixel.dfg.yyy, pixel.f0); #endif } /** * Returns the reflected vector at the current shading point. The reflected vector * return by this function might be different from shading.reflected: * - For anisotropic material, we bend the reflection vector to simulate * anisotropic indirect lighting * - The reflected vector may be modified to point towards the dominant specular * direction to match reference renderings when the roughness increases */ half3 getReflectedVector(const PixelParams pixel, const half3 v, const half3 n) { #if defined(MATERIAL_HAS_ANISOTROPY) half3 anisotropyDirection = pixel.anisotropy >= 0.0 ? pixel.anisotropicB : pixel.anisotropicT; half3 anisotropicTangent = cross(anisotropyDirection, v); half3 anisotropicNormal = cross(anisotropicTangent, anisotropyDirection); half bendFactor = abs(pixel.anisotropy) * saturate(5.0 * pixel.perceptualRoughness); half3 bentNormal = normalize(lerp(n, anisotropicNormal, bendFactor)); half3 r = reflect(-v, bentNormal); #else half3 r = reflect(-v, n); #endif return r; } half3 getReflectedVector(const ShadingParams shading, const PixelParams pixel, const half3 n) { #if defined(MATERIAL_HAS_ANISOTROPY) half3 r = getReflectedVector(pixel, shading.view, n); #else half3 r = shading.reflected; #endif return getSpecularDominantDirection(n, r, pixel.roughness); } //------------------------------------------------------------------------------ // Prefiltered importance sampling //------------------------------------------------------------------------------ #if IBL_INTEGRATION == IBL_INTEGRATION_IMPORTANCE_SAMPLING void isEvaluateClearCoatIBL(const ShadingParams shading, const PixelParams pixel, half specularAO, inout half3 Fd, inout half3 Fr) { #if defined(MATERIAL_HAS_CLEAR_COAT) #if defined(MATERIAL_HAS_NORMAL) || defined(MATERIAL_HAS_CLEAR_COAT_NORMAL) // We want to use the geometric normal for the clear coat layer half clearCoatNoV = clampNoV(dot(shading.clearCoatNormal, shading.view)); half3 clearCoatNormal = shading.clearCoatNormal; #else half clearCoatNoV = shading.NoV; half3 clearCoatNormal = shading.normal; #endif // The clear coat layer assumes an IOR of 1.5 (4% reflectance) half Fc = F_Schlick(0.04, 1.0, clearCoatNoV) * pixel.clearCoat; half attenuation = 1.0 - Fc; Fd *= attenuation; Fr *= attenuation; PixelParams p; p.perceptualRoughness = pixel.clearCoatPerceptualRoughness; p.f0 = 0.04; p.roughness = perceptualRoughnessToRoughness(p.perceptualRoughness); #if defined(MATERIAL_HAS_ANISOTROPY) p.anisotropy = 0.0; #endif half3 clearCoatLobe = isEvaluateSpecularIBL(p, clearCoatNormal, shading.view, clearCoatNoV); Fr += clearCoatLobe * (specularAO * pixel.clearCoat); #endif } #endif //------------------------------------------------------------------------------ // IBL evaluation //------------------------------------------------------------------------------ void evaluateClothIndirectDiffuseBRDF(const ShadingParams shading, const PixelParams pixel, inout half diffuse) { #if defined(SHADING_MODEL_CLOTH) #if defined(MATERIAL_HAS_SUBSURFACE_COLOR) // Simulate subsurface scattering with a wrap diffuse term diffuse *= Fd_Wrap(shading.NoV, 0.5); #endif #endif } void evaluateSheenIBL(const ShadingParams shading, const PixelParams pixel, const UnityGIInput unityData, half diffuseAO, inout half3 Fd, inout half3 Fr) { #if !defined(SHADING_MODEL_CLOTH) && !defined(SHADING_MODEL_SUBSURFACE) #if defined(MATERIAL_HAS_SHEEN_COLOR) // Albedo scaling of the base layer before we layer sheen on top Fd *= pixel.sheenScaling; Fr *= pixel.sheenScaling; half3 reflectance = pixel.sheenDFG * pixel.sheenColor; reflectance *= computeSpecularAO(shading.NoV, diffuseAO, pixel.sheenRoughness); Fr += reflectance * UnityGI_prefilteredRadiance(unityData, pixel.sheenPerceptualRoughness, shading.reflected); #endif #endif } void evaluateClearCoatIBL(const ShadingParams shading, const PixelParams pixel, const UnityGIInput unityData, half diffuseAO, inout half3 Fd, inout half3 Fr) { #if IBL_INTEGRATION == IBL_INTEGRATION_IMPORTANCE_SAMPLING half specularAO = computeSpecularAO(shading.NoV, diffuseAO, pixel.clearCoatRoughness); isEvaluateClearCoatIBL(pixel, specularAO, Fd, Fr); return; #endif #if defined(MATERIAL_HAS_CLEAR_COAT) #if defined(MATERIAL_HAS_NORMAL) || defined(MATERIAL_HAS_CLEAR_COAT_NORMAL) // We want to use the geometric normal for the clear coat layer half clearCoatNoV = clampNoV(dot(shading.clearCoatNormal, shading.view)); half3 clearCoatR = reflect(-shading.view, shading.clearCoatNormal); #else half clearCoatNoV = shading.NoV; half3 clearCoatR = shading.reflected; #endif // The clear coat layer assumes an IOR of 1.5 (4% reflectance) half Fc = F_Schlick(0.04, 1.0, clearCoatNoV) * pixel.clearCoat; half attenuation = 1.0 - Fc; Fd *= attenuation; Fr *= attenuation; // TODO: Should we apply specularAO to the attenuation as well? half specularAO = computeSpecularAO(clearCoatNoV, diffuseAO, pixel.clearCoatRoughness); Fr += UnityGI_prefilteredRadiance(unityData, pixel.clearCoatPerceptualRoughness, clearCoatR) * (specularAO * Fc); #endif } void evaluateSubsurfaceIBL(const ShadingParams shading, const PixelParams pixel, const UnityGIInput unityData, const float3 diffuseIrradiance, inout float3 Fd, inout float3 Fr) { #if defined(SHADING_MODEL_SUBSURFACE) float3 viewIndependent = diffuseIrradiance; half transmissionRoughness = saturate(pixel.perceptualRoughness + pixel.thickness); float3 viewDependent = UnityGI_prefilteredRadiance(unityData, transmissionRoughness, -shading.view); float attenuation = (1.0 - pixel.thickness) / (2.0 * PI); Fd += pixel.subsurfaceColor * (viewIndependent + viewDependent) * attenuation; #elif defined(SHADING_MODEL_CLOTH) && defined(MATERIAL_HAS_SUBSURFACE_COLOR) Fd *= saturate(pixel.subsurfaceColor + shading.NoV); #endif } #if defined(HAS_REFRACTION) struct Refraction { half3 position; half3 direction; half d; }; void refractionSolidSphere(const ShadingParams shading, const PixelParams pixel, const half3 n, half3 r, out Refraction ray) { r = refract(r, n, pixel.etaIR); half NoR = dot(n, r); half d = pixel.thickness * -NoR; ray.position = half3(shading.position + r * d); ray.d = d; half3 n1 = normalize(NoR * r - n * 0.5); ray.direction = refract(r, n1, pixel.etaRI); } void refractionSolidBox(const ShadingParams shading, const PixelParams pixel, const half3 n, half3 r, out Refraction ray) { half3 rr = refract(r, n, pixel.etaIR); half NoR = dot(n, rr); half d = pixel.thickness / max(-NoR, 0.001); ray.position = half3(shading.position + rr * d); ray.direction = r; ray.d = d; #if REFRACTION_MODE == REFRACTION_MODE_CUBEMAP // fudge direction vector, so we see the offset due to the thickness of the object half envDistance = 10.0; // this should come from a ubo ray.direction = normalize((ray.position - shading.position) + ray.direction * envDistance); #endif } void refractionThinSphere(const ShadingParams shading, const PixelParams pixel, const half3 n, half3 r, out Refraction ray) { half d = 0.0; #if defined(MATERIAL_HAS_MICRO_THICKNESS) // note: we need the refracted ray to calculate the distance traveled // we could use shading.NoV, but we would lose the dependency on ior. half3 rr = refract(r, n, pixel.etaIR); half NoR = dot(n, rr); d = pixel.uThickness / max(-NoR, 0.001); ray.position = half3(shading.position + rr * d); #else ray.position = half3(shading.position); #endif ray.direction = r; ray.d = d; } void applyRefraction( const ShadingParams shading, const PixelParams pixel, const UnityGIInput unityData, half3 E, half3 Fd, half3 Fr, inout half3 color) { Refraction ray; half iblLuminance = 1.0; // unused half refractionLodOffset = 0.0; // unused #if REFRACTION_TYPE == REFRACTION_TYPE_SOLID refractionSolidSphere(shading, pixel, shading.normal, -shading.view, ray); #elif REFRACTION_TYPE == REFRACTION_TYPE_THIN refractionThinSphere(shading, pixel, shading.normal, -shading.view, ray); #else #error invalid REFRACTION_TYPE #endif // compute transmission T #if defined(MATERIAL_HAS_ABSORPTION) #if defined(MATERIAL_HAS_THICKNESS) || defined(MATERIAL_HAS_MICRO_THICKNESS) half3 T = min(1.0, exp(-pixel.absorption * ray.d)); #else half3 T = 1.0 - pixel.absorption; #endif #endif // Roughness remapping so that an IOR of 1.0 means no microfacet refraction and an IOR // of 1.5 has full microfacet refraction half perceptualRoughness = lerp(pixel.perceptualRoughnessUnclamped, 0.0, saturate(pixel.etaIR * 3.0 - 2.0)); #if REFRACTION_TYPE == REFRACTION_TYPE_THIN // For thin surfaces, the light will bounce off at the second interface in the direction of // the reflection, effectively adding to the specular, but this process will repeat itself. // Each time the ray exits the surface on the front side after the first bounce, // it's multiplied by E^2, and we get: E + E(1-E)^2 + E^3(1-E)^2 + ... // This infinite series converges and is easy to simplify. // Note: we calculate these bounces only on a single component, // since it's a fairly subtle effect. E *= 1.0 + pixel.transmission * (1.0 - E.g) / (1.0 + E.g); #endif /* sample the cubemap or screen-space */ #if REFRACTION_MODE == REFRACTION_MODE_CUBEMAP // when reading from the cubemap, we are not pre-exposed so we apply iblLuminance // which is not the case when we'll read from the screen-space buffer half3 Ft = UnityGI_prefilteredRadiance(unityData, perceptualRoughness, ray.direction) * iblLuminance; #else // compute the point where the ray exits the medium, if needed //half4 p = half4(frameUniforms.clipFromWorldMatrix * half4(ray.position, 1.0)); //p.xy = uvToRenderTargetUV(p.xy * (0.5 / p.w) + 0.5); half4 p = UnityWorldToClipPos(half4(ray.position, 1.0)); p.w = (0.5 / p.w); p.xy = ComputeGrabScreenPos(p); // perceptualRoughness to LOD // Empirical factor to compensate for the gaussian approximation of Dggx, chosen so // cubemap and screen-space modes match at perceptualRoughness 0.125 // TODO: Remove this factor temporarily until we find a better solution // This overblurs many scenes and needs a more principled approach // half tweakedPerceptualRoughness = perceptualRoughness * 1.74; half tweakedPerceptualRoughness = perceptualRoughness; half lod = max(0.0, 2.0 * log2(tweakedPerceptualRoughness) + refractionLodOffset); half3 Ft = UNITY_SAMPLE_TEX2D_LOD(REFRACTION_SOURCE, p.xy, lod).rgb * REFRACTION_MULTIPLIER; #endif // base color changes the amount of light passing through the boundary Ft *= pixel.diffuseColor; // fresnel from the first interface Ft *= 1.0 - E; // apply absorption #if defined(MATERIAL_HAS_ABSORPTION) Ft *= T; #endif Fr *= iblLuminance; Fd *= iblLuminance; color.rgb += Fr + lerp(Fd, Ft, pixel.transmission); } #endif void combineDiffuseAndSpecular(const ShadingParams shading, const PixelParams pixel, const UnityGIInput unityData, const half3 E, const half3 Fd, const half3 Fr, inout half3 color) { const half iblLuminance = 1.0; // Unknown #if defined(HAS_REFRACTION) applyRefraction(shading, pixel, unityData, E, Fd, Fr, color); #else color.rgb += (Fd + Fr) * iblLuminance; #endif } void evaluateIBL(const ShadingParams shading, const MaterialInputs material, const PixelParams pixel, inout half3 color) { half ssao = 1.0; // Not implemented half lightmapAO = 1.0; // Specular-only AO derived from baked lighting and exposure occlusion setting half3 tangentNormal = half3(0, 0, 1); #if defined(MATERIAL_HAS_NORMAL) tangentNormal = material.normal; #endif Light derivedLight = (Light)0; // Gather Unity GI data UnityGIInput unityData = InitialiseUnityGIInput(shading, pixel); half3 unityIrradiance = UnityGI_Irradiance(shading, tangentNormal, /*out*/ lightmapAO, /*out*/ derivedLight); // specular layer half3 Fr; #if IBL_INTEGRATION == IBL_INTEGRATION_PREFILTERED_CUBEMAP half3 E = specularDFG(pixel); half3 r = getReflectedVector(shading, pixel, shading.normal); Fr = E * UnityGI_prefilteredRadiance(unityData, pixel.perceptualRoughness, r); #elif IBL_INTEGRATION == IBL_INTEGRATION_IMPORTANCE_SAMPLING // Not supported half3 E = half3(0.0); // TODO: fix for importance sampling Fr = isEvaluateSpecularIBL(pixel, shading.normal, shading.view, shading.NoV); #endif // Support for using a mirror as a specular reflection source. #if defined(MIRROR_REFLECTION) MirrorReflectionSampler mirrorSampler; half4 mirrorSpecular = mirrorSampler.getFilteredMirrorRadiance(shading, pixel.roughness); Fr = lerp(Fr, E * mirrorSpecular, mirrorSpecular.a); #endif // Ambient occlusion half diffuseAO = min(material.ambientOcclusion, ssao); half specularAO = computeSpecularAO(shading.NoV, diffuseAO*lightmapAO, pixel.roughness); half3 specularSingleBounceAO = singleBounceAO(specularAO) * pixel.energyCompensation; Fr *= specularSingleBounceAO; // Gather LTCGI data, if present. #if defined(_LTCGI) accumulator_struct acc = (accumulator_struct)0; LTCGI_Contribution( acc, shading.position, shading.normal, shading.view, pixel.perceptualRoughness, (shading.lightmapUV.xy - unity_LightmapST.zw) / unity_LightmapST.xy ); // Apply specular AO seperately for LTCGI pass, as it is a seperate set of lights. half ltc_specularAO = computeSpecularAO(shading.NoV, diffuseAO, pixel.roughness); half3 ltc_Fr = E * acc.specular; ltc_Fr *= singleBounceAO(ltc_specularAO) * pixel.energyCompensation; specularAO = lerp(specularAO, ltc_specularAO, saturate(acc.specularIntensity)); Fr = lerp(Fr, ltc_Fr, saturate(acc.specularIntensity)); #endif // diffuse layer half diffuseBRDF = singleBounceAO(diffuseAO); // Fd_Lambert() is baked in the SH below evaluateClothIndirectDiffuseBRDF(shading, pixel, diffuseBRDF); #if defined(MATERIAL_HAS_BENT_NORMAL) half3 diffuseNormal = shading.bentNormal; #else half3 diffuseNormal = shading.normal; #endif #if IBL_INTEGRATION == IBL_INTEGRATION_PREFILTERED_CUBEMAP //half3 diffuseIrradiance = get_diffuseIrradiance(diffuseNormal); half3 diffuseIrradiance = unityIrradiance; #elif IBL_INTEGRATION == IBL_INTEGRATION_IMPORTANCE_SAMPLING half3 diffuseIrradiance = isEvaluateDiffuseIBL(pixel, diffuseNormal, shading.view); #endif #if defined(_LTCGI) diffuseIrradiance += acc.diffuse; #endif // VRC Light Volumes also have an additive component which can be added over lightmapping. #if defined(_VRCLV) && !UNITY_SHOULD_SAMPLE_SH Light volumeLight = (Light)0; diffuseIrradiance += Irradiance_SampleVRCLightVolumeAdditive(shading.normal, shading.position, volumeLight); #endif half3 Fd = pixel.diffuseColor * diffuseIrradiance * (1.0 - E) * diffuseBRDF; // subsurface layer evaluateSubsurfaceIBL(shading, pixel, unityData, diffuseIrradiance, Fd, Fr); // extra ambient occlusion term for the base and subsurface layers multiBounceAO(diffuseAO, pixel.diffuseColor, Fd); multiBounceSpecularAO(specularAO, pixel.f0, Fr); // sheen layer evaluateSheenIBL(shading, pixel, unityData, diffuseAO, Fd, Fr); // clear coat layer evaluateClearCoatIBL(shading, pixel, unityData, diffuseAO, Fd, Fr); // Note: iblLuminance is already premultiplied by the exposure combineDiffuseAndSpecular(shading, pixel, unityData, E, Fd, Fr, color); #if defined(LIGHTMAP_SPECULAR) PixelParams pixelForBakedSpecular = pixel; // Remap roughness to clamp at max roughness without a hard clamp pixelForBakedSpecular.roughness = remap_almostIdentity(pixelForBakedSpecular.roughness, 1-getLightmapSpecularMaxSmoothness(), 1-getLightmapSpecularMaxSmoothness()+MIN_ROUGHNESS); // Remove diffuse component pixelForBakedSpecular.diffuseColor = 0; if (derivedLight.NoL >= 0.0) { // derived light contribution from lightmap half diffuseAOForLightmap = min(material.ambientOcclusion * 0.8 + 0.3, 1.0); diffuseAOForLightmap = computeMicroShadowing(derivedLight.NoL, diffuseAOForLightmap); color += max(0, surfaceShading(shading, pixelForBakedSpecular, derivedLight, diffuseAOForLightmap)); }; #if defined(_VRCLV) && !UNITY_SHOULD_SAMPLE_SH if (volumeLight.NoL >= 0.0) { // derived light contribution from lightmap half diffuseAOForLightmap = min(material.ambientOcclusion * 0.8 + 0.3, 1.0); diffuseAOForLightmap = computeMicroShadowing(volumeLight.NoL, diffuseAOForLightmap); color += max(0, surfaceShading(shading, pixelForBakedSpecular, volumeLight, diffuseAOForLightmap)); }; #endif #endif } #endif // FILAMENT_LIGHT_INDIRECT