From df6a100fcba66d9c99328a49d41fe3adecc0165d Mon Sep 17 00:00:00 2001 From: Mathias Agopian Date: Thu, 10 Feb 2022 15:52:04 -0800 Subject: [PATCH] Improve how we compute the lod from the roughness The main change here is that for screen-space reflections, the lod calculation takes the camera distance into account. We also now correctly take into account that we blur each lod successively which increases the standard deviation by 10%. This applies to both refraction and reflection. --- filament/src/PostProcessManager.cpp | 129 +++++++++++++++++++++------- filament/src/PostProcessManager.h | 4 +- shaders/src/common_lighting.fs | 9 ++ shaders/src/light_indirect.fs | 17 ++-- shaders/src/shading_lit.fs | 9 -- shaders/src/shading_reflections.fs | 2 +- 6 files changed, 119 insertions(+), 51 deletions(-) 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);