diff --git a/filament/src/PostProcessManager.cpp b/filament/src/PostProcessManager.cpp index 5932e60566..6b5105da22 100644 --- a/filament/src/PostProcessManager.cpp +++ b/filament/src/PostProcessManager.cpp @@ -881,9 +881,9 @@ FrameGraphId PostProcessManager::bilateralBlurPass( FrameGraphId PostProcessManager::generateGaussianMipmap(FrameGraph& fg, FrameGraphId input, size_t levels, - bool reinhard, size_t kernelWidth, float sigmaRatio) noexcept { + bool reinhard, size_t kernelWidth, float sigma) noexcept { for (size_t i = 1; i < levels; i++) { - input = gaussianBlurPass(fg, input, i - 1, input, i, 0, reinhard, kernelWidth, sigmaRatio); + input = gaussianBlurPass(fg, input, i - 1, input, i, 0, reinhard, kernelWidth, sigma); reinhard = false; // only do the reinhard filtering on the first level } return input; @@ -892,9 +892,7 @@ FrameGraphId PostProcessManager::generateGaussianMipmap(Frame FrameGraphId PostProcessManager::gaussianBlurPass(FrameGraph& fg, FrameGraphId input, uint8_t srcLevel, FrameGraphId output, uint8_t dstLevel, uint8_t layer, - bool reinhard, size_t kernelWidth, float sigmaRatio) noexcept { - - const float sigma = (float(kernelWidth) + 1.0f) / sigmaRatio; + bool reinhard, size_t kernelWidth, const float sigma) noexcept { Handle fullScreenRenderPrimitive = mEngine.getFullScreenRenderPrimitive(); @@ -1054,8 +1052,9 @@ FrameGraphId PostProcessManager::generateMipmapSSR(FrameGraph auto const& desc = fg.getDescriptor(input); - // scale factor for the gaussian so it matches our resolution / FOV - const float s = verticalFieldOfView / float(desc.height); + // texel size of the reflection buffer in world units at 1 meter + constexpr float d = 1.0f; // 1m + const float texelSizeAtOneMeter = d * std::tan(verticalFieldOfView) / float(desc.height); // The kernel-size was determined empirically so that we don't get too many artifacts // due to the down-sampling with a box filter (which happens implicitly). @@ -1067,29 +1066,99 @@ FrameGraphId PostProcessManager::generateMipmapSSR(FrameGraph static_assert(kernelSize & 1u, "kernel size must be odd"); static_assert((((kernelSize - 1u) / 2u) & 1u) == 0, "kernel positive side size must be even"); - // The relation between n and sigma (variance) is 6*sigma - 1 = N + // The relation between the kernel size N and sigma (standard deviation) is 6*sigma - 1 = N + // and is designed so the filter keeps its "gaussian-ness". + // The standard deviation sigma0 is expressed in texels. constexpr float sigma0 = (kernelSize + 1) / 6.0f; - // The variance doubles each time we go one mip down: - // sigma = sigma0 * 2^lod - // lod = log2(sigma) - log2(sigma0) = log2(sigma / sigma0). - // - // sigma is deduced from the roughness: - // roughness = sqrt(2) * s * sigma - // - // In the end we get: lod = 2 * log2(perceptualRoughness) - log2(sigma0 * s * sqrt2) + /* + * 1. Relation between standard deviation and LOD + * ---------------------------------------------- + * + * The standard deviation doubles at each level (i.e. variance quadruples), however, + * the mip-chain is constructed by successively blurring each level, which causes the + * variance of a given level to increase by the variance of the previous level (i.e. variances + * add under convolution). This results in a scaling of 2.23 (instead of 2) of the standard + * deviation for each level: sqrt( 1^2 + 2^2 ) = sqrt(5) = 2.23; + * + * The standard deviation is scaled by 2.23 each time we go one mip down, + * and our mipmap chain is built such that lod 0 is not blurred and lod 1 is blurred with + * sigma0 * 2 (because of the smaller resolution of lod 1). To simplify things a bit, we + * replace this factor by 2.23 (i.e. we pretend that lod 0 is blurred by sigma0). + * We then get: + * sigma = sigma0 * 2.23^lod + * lod = log2(sigma / sigma0) / log2(2.23) + * + * +------------------------------------------------+ + * | lod = [ log2(sigma) - log2(sigma0) ] * 0.8614 | + * +------------------------------------------------+ + * + * 2. Relation between standard deviation and roughness + * ---------------------------------------------------- + * + * The spherical gaussian approximation of the GGX distribution is given by: + * + * 1 2(cos(theta)-1) + * ------ exp( --------------- ) + * pi*a^2 a^2 + * + * + * Which is equivalent to: + * + * sqrt(2) + * ------- Gaussian(2 * sqrt(1 - cos(theta)), a) + * pi*a + * + * But when we filter a frame, we're actually calculating: + * + * Gaussian(d * tan(theta), sigma) + * + * With d the distance from the eye to the center sample, theta the angle, and it turns out + * that sqrt(2) * tan(theta) is very close to 2 * sqrt(1 - cos(theta)) for small angles, we + * can make that assumption because our filter is not wide. + * The above can be rewritten as: + * + * Gaussian(d * tan(theta), a * d / sqrt(2)) + * = Gaussian( tan(theta), a / sqrt(2)) + * + * Which now matches the SG approximation (we don't mind the scale factor because it's + * calculated automatically in the shader). + * + * We finally get that: + * + * +---------------------+ + * | sigma = a / sqrt(2) | + * +---------------------+ + * + * + * 3. Taking the resolution into account + * ------------------------------------- + * + * sigma0 above is expressed in texels, but we're interested in world units. The texel + * size in world unit is given by: + * + * +--------------------------------+ + * | s = d * tan(fov) / resolution | with d distance to camera plane + * +--------------------------------+ + * + * 4. Roughness to lod mapping + * --------------------------- + * + * Putting it all together we get: + * + * lod = [ log2(sigma) - log2( sigma0 * s ) ] * 0.8614 + * lod = [ log2(a / sqrt(2)) - log2( sigma0 * s ) ] * 0.8614 + * lod = [ log2(a) - log2( sqrt(2) * sigma0 * s ) ] * 0.8614 + * + * +-------------------------------------------------------------------------------------+ + * | lod = [ log2(a / d) - log2(sqrt(2) * sigma0 * (tan(fov) / resolution)) ] * 0.8614 | + * +-------------------------------------------------------------------------------------+ + */ - // FIXME: revisit the formula above. Not sure where the 2* comes from. - - const float refractionLodOffset = -std::log2(sigma0 * s * f::SQRT2); - const float maxPerceptualRoughness = 0.5f; - const uint8_t maxLod = std::ceil(2.0f * std::log2(maxPerceptualRoughness) + refractionLodOffset); - - // Number of roughness levels we want. - // TODO: If we want to limit the number of mip levels, we must reduce the initial - // resolution (if we want to keep the same filter, and still match the IBL somewhat). - const uint8_t roughnessLodCount = - std::min(maxLod, FTexture::maxLevelCount(desc.width, desc.height)); + const float refractionLodOffset = -std::log2(f::SQRT2 * sigma0 * texelSizeAtOneMeter); + uint8_t roughnessLodCount = FTexture::maxLevelCount(desc.width, desc.height); + // we don't go lower than 16 texel in one dimension + roughnessLodCount = std::max(std::min(4, +roughnessLodCount), +roughnessLodCount - 4); // Then copy the color buffer into a texture, making sure that we keep the original // buffer aspect-ratio (this is because dynamic-resolution is not necessarily homogenous). @@ -1141,7 +1210,7 @@ FrameGraphId PostProcessManager::generateMipmapSSR(FrameGraph */ *pLodOffset = refractionLodOffset; - input = ppm.generateGaussianMipmap(fg, input, roughnessLodCount, true, kernelSize); + input = ppm.generateGaussianMipmap(fg, input, roughnessLodCount, true, kernelSize, sigma0); return input; } @@ -1859,10 +1928,12 @@ FrameGraphId PostProcessManager::bloomPass(FrameGraph& fg, commitAndRender(out, material, driver); }); + constexpr float kernelWidth = 9; + constexpr float sigma = (kernelWidth + 1.0f) / 6.0f; auto flare = gaussianBlurPass(fg, flarePass->out, 0, {}, 0, 0, - false, 9); + false, kernelWidth, sigma); fg.getBlackboard().put("flare", flare); diff --git a/filament/src/PostProcessManager.h b/filament/src/PostProcessManager.h index 12f47de6c2..47cd5d871e 100644 --- a/filament/src/PostProcessManager.h +++ b/filament/src/PostProcessManager.h @@ -93,7 +93,7 @@ public: // Gaussian mipmap FrameGraphId generateGaussianMipmap(FrameGraph& fg, FrameGraphId input, size_t levels, bool reinhard, - size_t kernelWidth, float sigmaRatio = 6.0f) noexcept; + size_t kernelWidth, float sigma) noexcept; FrameGraphId generateMipmapSSR(FrameGraph& fg, FrameGraphId input, float verticalFieldOfView, @@ -176,7 +176,7 @@ public: FrameGraphId gaussianBlurPass(FrameGraph& fg, FrameGraphId input, uint8_t srcLevel, FrameGraphId output, uint8_t dstLevel, uint8_t layer, - bool reinhard, size_t kernelWidth, float sigma = 6.0f) noexcept; + bool reinhard, size_t kernelWidth, float sigma) noexcept; backend::Handle getOneTexture() const; backend::Handle getZeroTexture() const; diff --git a/shaders/src/common_lighting.fs b/shaders/src/common_lighting.fs index 06bce3cdfe..787bfea6a8 100644 --- a/shaders/src/common_lighting.fs +++ b/shaders/src/common_lighting.fs @@ -94,3 +94,12 @@ vec3 getReflectedVector(const PixelParams pixel, const vec3 v, const vec3 n) { #endif return r; } + +void getAnisotropyPixelParams(const MaterialInputs material, inout PixelParams pixel) { +#if defined(MATERIAL_HAS_ANISOTROPY) + vec3 direction = material.anisotropyDirection; + pixel.anisotropy = material.anisotropy; + pixel.anisotropicT = normalize(shading_tangentToWorld * direction); + pixel.anisotropicB = normalize(cross(getWorldGeometricNormalVector(), pixel.anisotropicT)); +#endif +} diff --git a/shaders/src/light_indirect.fs b/shaders/src/light_indirect.fs index 010f7f53f0..d8547e62db 100644 --- a/shaders/src/light_indirect.fs +++ b/shaders/src/light_indirect.fs @@ -520,15 +520,9 @@ vec3 evaluateRefraction( vec4 p = vec4(getClipFromWorldMatrix() * vec4(ray.position, 1.0)); p.xy = uvToRenderTargetUV(p.xy * (0.5 / p.w) + 0.5); - // 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 - // float tweakedPerceptualRoughness = perceptualRoughness * 1.74; - float tweakedPerceptualRoughness = perceptualRoughness; - float lod = max(0.0, 2.0 * log2(tweakedPerceptualRoughness) + frameUniforms.refractionLodOffset); - + // distance to camera plane + const float invLog2sqrt5 = 0.8614; + float lod = max(0.0, (2.0f * log2(perceptualRoughness) + frameUniforms.refractionLodOffset) * invLog2sqrt5); Ft = textureLod(light_ssr, p.xy, lod).rgb; #endif @@ -567,7 +561,10 @@ void evaluateIBL(const MaterialInputs material, const PixelParams pixel, inout v // TODO: maybe make this a parameter const float maxPerceptualRoughness = sqrt(0.5); if (pixel.perceptualRoughness < maxPerceptualRoughness) { - float lod = max(0.0, 2.0 * log2(pixel.perceptualRoughness) + frameUniforms.refractionLodOffset); + // distance to camera plane + const float invLog2sqrt5 = 0.8614; + float d = -mulMat4x4Float3(getViewFromWorldMatrix(), getWorldPosition()).z; + float lod = max(0.0, (log2(pixel.roughness / d) + frameUniforms.refractionLodOffset) * invLog2sqrt5); #if !defined(MATERIAL_HAS_REFRACTION) // this is temporary, until we can access the SSR buffer when we have refraction ssrFr = textureLod(light_ssr, interpolationCache.uv, lod); diff --git a/shaders/src/shading_lit.fs b/shaders/src/shading_lit.fs index f5e96d2e38..944666554b 100644 --- a/shaders/src/shading_lit.fs +++ b/shaders/src/shading_lit.fs @@ -206,15 +206,6 @@ void getSubsurfacePixelParams(const MaterialInputs material, inout PixelParams p #endif } -void getAnisotropyPixelParams(const MaterialInputs material, inout PixelParams pixel) { -#if defined(MATERIAL_HAS_ANISOTROPY) - vec3 direction = material.anisotropyDirection; - pixel.anisotropy = material.anisotropy; - pixel.anisotropicT = normalize(shading_tangentToWorld * direction); - pixel.anisotropicB = normalize(cross(getWorldGeometricNormalVector(), pixel.anisotropicT)); -#endif -} - void getEnergyCompensationPixelParams(inout PixelParams pixel) { // Pre-filtered DFG term used for image-based lighting pixel.dfg = prefilteredDFG(pixel.perceptualRoughness, shading_NoV); diff --git a/shaders/src/shading_reflections.fs b/shaders/src/shading_reflections.fs index 3f6bca12b5..dd1b81277d 100644 --- a/shaders/src/shading_reflections.fs +++ b/shaders/src/shading_reflections.fs @@ -5,7 +5,7 @@ vec4 evaluateMaterial(const MaterialInputs material) { #if defined(MATERIAL_HAS_REFLECTIONS) PixelParams pixel; - //getAnisotropyPixelParams(inputs, pixel); + getAnisotropyPixelParams(material, pixel); vec4 color = vec4(0.0); if (frameUniforms.ssrDistance > 0.0) { vec3 r = getReflectedVector(pixel, shading_view, shading_normal);