diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 9423dd3c1d..ba179c683d 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -4,7 +4,9 @@ This file contains one line summaries of commits that are worthy of mentioning i A new header is inserted each time a *tag* is created. ## v1.10.6 (currently main branch) + - engine: Use exponential VSM and improve VSM user settings [⚠️ **Recompile Materials for VSM**]. +- engine: Optional blurring of VSM shadowmaps ## v1.10.5 diff --git a/android/filament-android/src/main/cpp/LightManager.cpp b/android/filament-android/src/main/cpp/LightManager.cpp index 72bf5dfa4e..228393ff13 100644 --- a/android/filament-android/src/main/cpp/LightManager.cpp +++ b/android/filament-android/src/main/cpp/LightManager.cpp @@ -77,7 +77,7 @@ Java_com_google_android_filament_LightManager_nBuilderShadowOptions(JNIEnv* env, jlong nativeBuilder, jint mapSize, jint cascades, jfloatArray splitPositions, jfloat constantBias, jfloat normalBias, jfloat shadowFar, jfloat shadowNearHint, jfloat shadowFarHint, jboolean stable, jboolean screenSpaceContactShadows, jint stepCount, - jfloat maxShadowDistance, jint vsmMsaaSamples) { + jfloat maxShadowDistance, jint vsmMsaaSamples, jfloat blurStandardDeviation) { LightManager::Builder *builder = (LightManager::Builder *) nativeBuilder; LightManager::ShadowOptions shadowOptions { .mapSize = (uint32_t)mapSize, @@ -92,7 +92,8 @@ Java_com_google_android_filament_LightManager_nBuilderShadowOptions(JNIEnv* env, .stepCount = uint8_t(stepCount), .maxShadowDistance = maxShadowDistance, .vsm = { - .msaaSamples = (uint8_t) vsmMsaaSamples + .msaaSamples = (uint8_t) vsmMsaaSamples, + .blurStandardDeviation = blurStandardDeviation } }; jfloat *nativeSplits = env->GetFloatArrayElements(splitPositions, NULL); diff --git a/android/filament-android/src/main/java/com/google/android/filament/LightManager.java b/android/filament-android/src/main/java/com/google/android/filament/LightManager.java index 3b8629fc2e..02acc99e4f 100644 --- a/android/filament-android/src/main/java/com/google/android/filament/LightManager.java +++ b/android/filament-android/src/main/java/com/google/android/filament/LightManager.java @@ -326,6 +326,18 @@ public class LightManager { */ @IntRange(from = 1) public int vsmMsaaSamples = 1; + + /** + * Standard deviation of the VSM blur. Zero do disable. + * The maximum value is 21, which corresponds to a gaussian blur filter width + * of 125 pixels. The relation between the filter width and the standard deviation + * is roughly: stddev = (kernelWidth + 1) / 6. + * Some common values for blurStandardDeviation: + * 3x3 gaussian : 0.6667 + * 5x5 gaussian : 1.0 + * 9x9 gaussian : 1.6667 + */ + public float blurStandardDeviation = 0.0f; } public static class ShadowCascades { @@ -454,7 +466,8 @@ public class LightManager { options.mapSize, options.shadowCascades, options.cascadeSplitPositions, options.constantBias, options.normalBias, options.shadowFar, options.shadowNearHint, options.shadowFarHint, options.stable, options.screenSpaceContactShadows, - options.stepCount, options.maxShadowDistance, options.vsmMsaaSamples); + options.stepCount, options.maxShadowDistance, options.vsmMsaaSamples, + options.blurStandardDeviation); return this; } @@ -1088,7 +1101,7 @@ public class LightManager { private static native void nDestroyBuilder(long nativeBuilder); private static native boolean nBuilderBuild(long nativeBuilder, long nativeEngine, int entity); private static native void nBuilderCastShadows(long nativeBuilder, boolean enable); - private static native void nBuilderShadowOptions(long nativeBuilder, int mapSize, int cascades, float[] splitPositions, float constantBias, float normalBias, float shadowFar, float shadowNearHint, float shadowFarhint, boolean stable, boolean screenSpaceContactShadows, int stepCount, float maxShadowDistance, int vsmMsaaSamples); + private static native void nBuilderShadowOptions(long nativeBuilder, int mapSize, int cascades, float[] splitPositions, float constantBias, float normalBias, float shadowFar, float shadowNearHint, float shadowFarhint, boolean stable, boolean screenSpaceContactShadows, int stepCount, float maxShadowDistance, int vsmMsaaSamples, float blurStandardDeviation); private static native void nBuilderCastLight(long nativeBuilder, boolean enabled); private static native void nBuilderPosition(long nativeBuilder, float x, float y, float z); private static native void nBuilderDirection(long nativeBuilder, float x, float y, float z); diff --git a/filament/include/filament/LightManager.h b/filament/include/filament/LightManager.h index 93113bbd00..bd70d7a42b 100644 --- a/filament/include/filament/LightManager.h +++ b/filament/include/filament/LightManager.h @@ -325,6 +325,18 @@ public: * Higher values may not be available depending on the underlying hardware. */ uint8_t msaaSamples = 1; + + /** + * Standard deviation of the VSM blur. Zero do disable. + * The maximum value is 21, which corresponds to a gaussian blur filter width + * of 125 pixels. The relation between the filter width and the standard deviation + * is roughly: stddev = (kernelWidth + 1) / 6. + * Some common values for blurStandardDeviation: + * 3x3 gaussian : 0.6667 + * 5x5 gaussian : 1.0 + * 9x9 gaussian : 1.6667 + */ + float blurStandardDeviation = 0.0f; } vsm; }; diff --git a/filament/src/Material.cpp b/filament/src/Material.cpp index 07654b8c9e..2471e2ab80 100644 --- a/filament/src/Material.cpp +++ b/filament/src/Material.cpp @@ -344,11 +344,7 @@ bool FMaterial::isSampler(const char* name) const noexcept { UniformInterfaceBlock::UniformInfo const* FMaterial::reflect( utils::StaticString const& name) const noexcept { - auto const& list = mUniformInterfaceBlock.getUniformInfoList(); - auto p = std::find_if(list.begin(), list.end(), [&](auto const& e) { - return e.name == name; - }); - return p == list.end() ? nullptr : &static_cast(*p); + return mUniformInterfaceBlock.getUniformInfo(name.c_str()); } Handle FMaterial::getProgramSlow(uint8_t variantKey) const noexcept { diff --git a/filament/src/PostProcessManager.cpp b/filament/src/PostProcessManager.cpp index a121280455..1580b3c299 100644 --- a/filament/src/PostProcessManager.cpp +++ b/filament/src/PostProcessManager.cpp @@ -200,30 +200,30 @@ struct MaterialInfo { }; static const MaterialInfo sMaterialList[] = { - { "sao", MATERIAL(SAO) }, - { "mipmapDepth", MATERIAL(MIPMAPDEPTH) }, - { "vsmMipmap", MATERIAL(VSMMIPMAP) }, - { "bilateralBlur", MATERIAL(BILATERALBLUR) }, - { "separableGaussianBlur", MATERIAL(SEPARABLEGAUSSIANBLUR) }, - { "bloomDownsample", MATERIAL(BLOOMDOWNSAMPLE) }, - { "bloomUpsample", MATERIAL(BLOOMUPSAMPLE) }, - { "flare", MATERIAL(FLARE) }, - { "blitLow", MATERIAL(BLITLOW) }, - { "blitMedium", MATERIAL(BLITMEDIUM) }, - { "blitHigh", MATERIAL(BLITHIGH) }, - { "colorGrading", MATERIAL(COLORGRADING) }, - { "colorGradingAsSubpass", MATERIAL(COLORGRADINGASSUBPASS) }, - { "fxaa", MATERIAL(FXAA) }, - { "taa", MATERIAL(TAA) }, - { "dofDownsample", MATERIAL(DOFDOWNSAMPLE) }, - { "dofCoc", MATERIAL(DOFCOC) }, - { "dofMipmap", MATERIAL(DOFMIPMAP) }, - { "dofTiles", MATERIAL(DOFTILES) }, - { "dofTilesSwizzle", MATERIAL(DOFTILESSWIZZLE) }, - { "dofDilate", MATERIAL(DOFDILATE) }, - { "dof", MATERIAL(DOF) }, - { "dofMedian", MATERIAL(DOFMEDIAN) }, - { "dofCombine", MATERIAL(DOFCOMBINE) }, + { "bilateralBlur", MATERIAL(BILATERALBLUR) }, + { "blitHigh", MATERIAL(BLITHIGH) }, + { "blitLow", MATERIAL(BLITLOW) }, + { "blitMedium", MATERIAL(BLITMEDIUM) }, + { "bloomDownsample", MATERIAL(BLOOMDOWNSAMPLE) }, + { "bloomUpsample", MATERIAL(BLOOMUPSAMPLE) }, + { "colorGrading", MATERIAL(COLORGRADING) }, + { "colorGradingAsSubpass", MATERIAL(COLORGRADINGASSUBPASS) }, + { "dof", MATERIAL(DOF) }, + { "dofCoc", MATERIAL(DOFCOC) }, + { "dofCombine", MATERIAL(DOFCOMBINE) }, + { "dofDilate", MATERIAL(DOFDILATE) }, + { "dofDownsample", MATERIAL(DOFDOWNSAMPLE) }, + { "dofMedian", MATERIAL(DOFMEDIAN) }, + { "dofMipmap", MATERIAL(DOFMIPMAP) }, + { "dofTiles", MATERIAL(DOFTILES) }, + { "dofTilesSwizzle", MATERIAL(DOFTILESSWIZZLE) }, + { "flare", MATERIAL(FLARE) }, + { "fxaa", MATERIAL(FXAA) }, + { "mipmapDepth", MATERIAL(MIPMAPDEPTH) }, + { "sao", MATERIAL(SAO) }, + { "separableGaussianBlur", MATERIAL(SEPARABLEGAUSSIANBLUR) }, + { "taa", MATERIAL(TAA) }, + { "vsmMipmap", MATERIAL(VSMMIPMAP) }, }; void PostProcessManager::init() noexcept { @@ -235,15 +235,6 @@ void PostProcessManager::init() noexcept { registerPostProcessMaterial(info.name, info.data, info.size); } - // UBO storage size. - // The effective kernel size is (kMaxPositiveKernelSize - 1) * 4 + 1. - // e.g.: 5 positive-side samples, give 4+1+4=9 samples both sides - // taking advantage of linear filtering produces an effective kernel of 8+1+8=17 samples - // and because it's a separable filter, the effective 2D filter kernel size is 17*17 - // The total number of samples needed over the two passes is 18. - auto& separableGaussianBlur = getPostProcessMaterial("separableGaussianBlur"); - mSeparableGaussianBlurKernelStorageSize = separableGaussianBlur.getMaterial()->reflect("kernel")->size; - mDummyOneTexture = driver.createTexture(SamplerType::SAMPLER_2D, 1, TextureFormat::RGBA8, 1, 1, 1, 1, TextureUsage::DEFAULT); @@ -683,7 +674,7 @@ FrameGraphId PostProcessManager::generateGaussianMipmap(Frame FrameGraphId input, size_t roughnessLodCount, bool reinhard, size_t kernelWidth, float sigmaRatio) noexcept { for (size_t i = 1; i < roughnessLodCount; i++) { - input = gaussianBlurPass(fg, input, i - 1, input, i, reinhard, kernelWidth, sigmaRatio); + input = gaussianBlurPass(fg, input, i - 1, input, i, 0, reinhard, kernelWidth, sigmaRatio); reinhard = false; // only do the reinhard filtering on the first level } return input; @@ -691,7 +682,7 @@ FrameGraphId PostProcessManager::generateGaussianMipmap(Frame FrameGraphId PostProcessManager::gaussianBlurPass(FrameGraph& fg, FrameGraphId input, uint8_t srcLevel, - FrameGraphId output, uint8_t dstLevel, + FrameGraphId output, uint8_t dstLevel, uint8_t layer, bool reinhard, size_t kernelWidth, float sigmaRatio) noexcept { const float sigma = (kernelWidth + 1.0f) / sigmaRatio; @@ -739,7 +730,12 @@ FrameGraphId PostProcessManager::gaussianBlurPass(FrameGraph& FrameGraphId temp; }; - const size_t kernelStorageSize = mSeparableGaussianBlurKernelStorageSize; + // The effective kernel size is (kMaxPositiveKernelSize - 1) * 4 + 1. + // e.g.: 5 positive-side samples, give 4+1+4=9 samples both sides + // taking advantage of linear filtering produces an effective kernel of 8+1+8=17 samples + // and because it's a separable filter, the effective 2D filter kernel size is 17*17 + // The total number of samples needed over the two passes is 18. + fg.addPass("Gaussian Blur Passes", [&](FrameGraph::Builder& builder, auto& data) { auto desc = builder.getDescriptor(input); @@ -761,7 +757,7 @@ FrameGraphId PostProcessManager::gaussianBlurPass(FrameGraph& data.temp = builder.sample(data.temp); data.temp = builder.declareRenderPass(data.temp); - data.out = builder.createSubresource(output, "Blurred texture mip",{ .level = dstLevel }); + data.out = builder.createSubresource(output, "Blurred texture mip",{ .level = dstLevel, .layer = layer }); data.out = builder.declareRenderPass(data.out); }, [=](FrameGraphResources const& resources, @@ -769,6 +765,7 @@ FrameGraphId PostProcessManager::gaussianBlurPass(FrameGraph& auto const& separableGaussianBlur = getPostProcessMaterial("separableGaussianBlur"); FMaterialInstance* const mi = separableGaussianBlur.getMaterialInstance(); + const size_t kernelStorageSize = mi->getMaterial()->reflect("kernel")->size; float2 kernel[64]; size_t m = computeGaussianCoefficients(kernel, @@ -1535,8 +1532,10 @@ FrameGraphId PostProcessManager::bloomPass(FrameGraph& fg, commitAndRender(out, material, driver); }); - auto flare = gaussianBlurPass(fg, flarePass->out, 0, - {}, 0, false, 9); + auto flare = gaussianBlurPass(fg, + flarePass->out, 0, + {}, 0, 0, + false, 9); fg.getBlackboard().put("flare", flare); diff --git a/filament/src/PostProcessManager.h b/filament/src/PostProcessManager.h index 6c6eaaaeb4..8a1b45b2cb 100644 --- a/filament/src/PostProcessManager.h +++ b/filament/src/PostProcessManager.h @@ -133,6 +133,11 @@ public: FrameGraphId vsmMipmapPass(FrameGraph& fg, FrameGraphId input, uint8_t layer, size_t level, bool finalize) noexcept; + 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; + backend::Handle getOneTexture() const { return mDummyOneTexture; } backend::Handle getZeroTexture() const { return mDummyZeroTexture; } backend::Handle getOneTextureArray() const { return mDummyOneTextureArray; } @@ -156,11 +161,6 @@ private: FrameGraph& fg, FrameGraphId input, math::int2 axis, float zf, backend::TextureFormat format, BilateralPassConfig config) noexcept; - FrameGraphId gaussianBlurPass(FrameGraph& fg, - FrameGraphId input, uint8_t srcLevel, - FrameGraphId output, uint8_t dstLevel, - bool reinhard, size_t kernelWidth, float sigma = 6.0f) noexcept; - FrameGraphId bloomPass(FrameGraph& fg, FrameGraphId input, backend::TextureFormat outFormat, View::BloomOptions& bloomOptions, math::float2 scale) noexcept; @@ -220,8 +220,6 @@ private: backend::Handle mDummyZeroTexture; backend::Handle mStarburstTexture; - size_t mSeparableGaussianBlurKernelStorageSize = 0; - std::uniform_real_distribution mUniformDistribution{0.0f, 1.0f}; const math::float2 mHaltonSamples[16]; diff --git a/filament/src/ShadowMapManager.cpp b/filament/src/ShadowMapManager.cpp index 90fdb72b8e..a362098cc3 100644 --- a/filament/src/ShadowMapManager.cpp +++ b/filament/src/ShadowMapManager.cpp @@ -81,14 +81,16 @@ void ShadowMapManager::render(FrameGraph& fg, FEngine& engine, FView& view, constexpr size_t MAX_SHADOW_LAYERS = CONFIG_MAX_SHADOW_CASCADES + CONFIG_MAX_SHADOW_CASTING_SPOTS; struct ShadowPassData { - FrameGraphId shadows; - FrameGraphId tempDepth; - uint32_t rt[MAX_SHADOW_LAYERS]; + FrameGraphId shadows; // the actual shadowmap + FrameGraphId tempBlurSrc[MAX_SHADOW_LAYERS]; // temporary shadowmap when blurring + FrameGraphId tempDepth; // temporary depth for VSM + uint32_t rt[MAX_SHADOW_LAYERS]; // RT for each layer of 'shadows' + uint32_t trt[MAX_SHADOW_LAYERS]; // RT for each tempBlurSrc, needed for MSAA resolve }; using ShadowPass = std::pair; auto passes = utils::FixedCapacityVector::with_capacity(MAX_SHADOW_LAYERS); - uint8_t layerSampleCount[MAX_SHADOW_LAYERS] = {}; + LightManager::ShadowOptions const *options[MAX_SHADOW_LAYERS] = {}; // make a copy here, because it's a very small structure TextureRequirements const textureRequirements = mTextureRequirements; @@ -97,37 +99,36 @@ void ShadowMapManager::render(FrameGraph& fg, FEngine& engine, FView& view, // These loops fill render passes with appropriate rendering commands for each shadow map. // The actual render pass execution is deferred to the frame graph. + + // Directional, cascaded shadowmaps for (const auto& map : mCascadeShadowMaps) { if (!map.hasVisibleShadows()) { continue; } - map.getShadowMap().render(driver, view.getVisibleDirectionalShadowCasters(), pass, view); - assert_invariant(map.getLayout().layer < textureRequirements.layers); passes.emplace_back(&map, pass); - const uint8_t layer = map.getLayout().layer; assert_invariant(layer < MAX_SHADOW_LAYERS); - layerSampleCount[layer] = map.getLayout().vsmSamples; + options[layer] = map.getLayout().options; } - for (size_t i = 0; i < mSpotShadowMaps.size(); i++) { + + // Spotlight shadowmaps + for (size_t i = 0, c = mSpotShadowMaps.size(); i < c; i++) { const auto& map = mSpotShadowMaps[i]; if (!map.hasVisibleShadows()) { continue; } - pass.setVisibilityMask(VISIBLE_SPOT_SHADOW_RENDERABLE_N(i)); map.getShadowMap().render(driver, view.getVisibleSpotShadowCasters(), pass, view); pass.clearVisibilityMask(); - assert_invariant(map.getLayout().layer < textureRequirements.layers); passes.emplace_back(&map, pass); - const uint8_t layer = map.getLayout().layer; assert_invariant(layer < MAX_SHADOW_LAYERS); - layerSampleCount[layer] = map.getLayout().vsmSamples; + options[layer] = map.getLayout().options; } + assert_invariant(passes.size() <= textureRequirements.layers); const bool fillWithCheckerboard = engine.debug.shadowmap.checkerboard && !view.hasVsm(); @@ -139,71 +140,107 @@ void ShadowMapManager::render(FrameGraph& fg, FEngine& engine, FView& view, .depth = textureRequirements.layers, .levels = textureRequirements.levels, .type = SamplerType::SAMPLER_2D_ARRAY, - .format = mTextureFormat + .format = view.hasVsm() ? TextureFormat::RG16F : mTextureFormat }; - if (view.hasVsm()) { - shadowTextureDesc.format = TextureFormat::RG16F; - } - - data.shadows = builder.createTexture("Shadow Texture", shadowTextureDesc); + data.shadows = builder.createTexture("Shadowmap", shadowTextureDesc); if (view.hasVsm()) { - // When rendering VSM shadow maps, we still need a depth texture for correct - // sorting. The texture is cleared before each pass and discarded afterwards. + // Each shadow pass has its own sample count, but textures are created with + // a default count of 1 because we're using "magic resolve" (sample count is + // set on the render target). + + // When rendering VSM shadow maps, we still need a depth texture for sorting. data.tempDepth = builder.createTexture("Temporary VSM Depth Texture", { .width = textureRequirements.size, .height = textureRequirements.size, - .depth = 1, - .levels = 1, - // Each shadow pass has its own sample count. We specify samples = 1 here to - // force the frame graph to create the "magic resolve" textures with correct - // sample counts automatically. - .samples = 1, .type = SamplerType::SAMPLER_2D, - .format = mTextureFormat, // use the same format that we'd use for regular shadows + .format = mTextureFormat, }); } // Create a render target for each layer of the texture array. for (uint8_t i = 0u; i < textureRequirements.layers; i++) { - FrameGraphRenderPass::Descriptor renderTargetDesc {}; + if (!options[i]) continue; + + FrameGraphRenderPass::Descriptor renderTargetDesc{}; + + auto attachment = builder.createSubresource(data.shadows, + "Shadowmap Layer", { .layer = i }); + if (view.hasVsm()) { - auto attachment = builder.createSubresource(data.shadows, "Shadow Texture Mip", { .layer = i }); - attachment = builder.write(attachment, FrameGraphTexture::Usage::COLOR_ATTACHMENT); - data.tempDepth = builder.write(data.tempDepth, FrameGraphTexture::Usage::DEPTH_ATTACHMENT); + // Temporary (resolved) texture used to render the shadowmap when blurring + // is needed -- it'll be used as the source of the blur. + data.tempBlurSrc[i] = builder.createTexture("Temporary Shadowmap",{ + .width = textureRequirements.size, .height = textureRequirements.size, + .type = SamplerType::SAMPLER_2D, + .format = TextureFormat::RG16F + }); + + // the shadowmap layer + attachment = builder.write(attachment, + FrameGraphTexture::Usage::COLOR_ATTACHMENT); + + // the depth buffer + data.tempDepth = builder.write(data.tempDepth, + FrameGraphTexture::Usage::DEPTH_ATTACHMENT); + renderTargetDesc.attachments = { .color = { attachment }, .depth = data.tempDepth }; renderTargetDesc.clearFlags = TargetBufferFlags::COLOR | TargetBufferFlags::DEPTH; // we need to clear the shadow map with the max EVSM moments - renderTargetDesc.clearColor = { 256.0f, 65536.f, 256.0f, 65536.f }; - renderTargetDesc.samples = layerSampleCount[i]; + renderTargetDesc.clearColor = { 256.0f, 65536.f, 0.0f, 0.0f }; + renderTargetDesc.samples = options[i]->vsm.msaaSamples; + + if (options[i]->vsm.blurStandardDeviation > 0.0f) { + data.tempBlurSrc[i] = builder.write(data.tempBlurSrc[i], + FrameGraphTexture::Usage::COLOR_ATTACHMENT); + + data.trt[i] = builder.declareRenderPass("Temp Shadow RT", { + .attachments = { + .color = { data.tempBlurSrc[i] }, + .depth = data.tempDepth }, + .clearColor = { 256.0f, 65536.f, 256.0f, 65536.f }, + .samples = options[i]->vsm.msaaSamples, + .clearFlags = TargetBufferFlags::COLOR | TargetBufferFlags::DEPTH + }); + } } else { - auto attachment = builder.createSubresource(data.shadows, "Shadow Texture Mip", { .layer = i }); - attachment = builder.write(attachment, FrameGraphTexture::Usage::DEPTH_ATTACHMENT); + // the shadowmap layer + attachment = builder.write(attachment, + FrameGraphTexture::Usage::DEPTH_ATTACHMENT); renderTargetDesc.attachments = { .depth = attachment }; renderTargetDesc.clearFlags = TargetBufferFlags::DEPTH; } + // finally create the shadowmap render target -- one per layer. data.rt[i] = builder.declareRenderPass("Shadow RT", renderTargetDesc); } }, [=, passes = std::move(passes), &view, &engine](FrameGraphResources const& resources, auto const& data, DriverApi& driver) mutable { for (auto& [map, pass] : passes) { + if (!map->hasVisibleShadows()) continue; + + ShadowLayout const& layout = map->getLayout(); + const auto layer = layout.layer; + const auto& options = layout.options; + const bool blur = view.hasVsm() && options->vsm.blurStandardDeviation > 0.0f; + // TODO: camera is already set inside 'pass', we could get it from there FCamera const& camera = map->getShadowMap().getCamera(); filament::CameraInfo cameraInfo(camera); view.prepareCamera(cameraInfo); - // we set a viewport with a 1-texel border for when we index outside of the - // texture + // We set a viewport with a 1-texel border for when we index outside of the + // texture. // DON'T CHANGE this unless ShadowMap::getTextureCoordsMapping() is updated too. // see: ShadowMap::getTextureCoordsMapping() + // // For floating-point depth textures, the 1-texel border could be set to // FLOAT_MAX to avoid clamping in the shadow shader (see sampleDepth inside // shadowing.fs). Unfortunately, the APIs don't seem let us clear depth // attachments to anything greater than 1.0, so we'd need a way to do this other // than clearing. - const uint32_t dim = map->getLayout().size; + const uint32_t dim = options->mapSize; filament::Viewport viewport { 1, 1, dim - 2, dim - 2 }; view.prepareViewport(viewport); @@ -214,8 +251,10 @@ void ShadowMapManager::render(FrameGraph& fg, FEngine& engine, FView& view, view.commitUniforms(driver); - const auto layer = map->getLayout().layer; - auto rt = resources.getRenderPassInfo(data.rt[layer]); + // render either directly into the shadowmap, or to the temporary texture for + // blurring. + auto rt = resources.getRenderPassInfo(blur ? data.trt[layer] : data.rt[layer]); + rt.params.viewport = viewport; auto polygonOffset = map->getShadowMap().getPolygonOffset(); @@ -249,14 +288,34 @@ void ShadowMapManager::render(FrameGraph& fg, FEngine& engine, FView& view, shadows = debugPatternPass.getData().shadows; } - // If the shadow texture has more than one level, mipmapping was requested, either directly - // or indirectly via anisotropic filtering. - if (textureRequirements.levels > 1) { - auto& ppm = engine.getPostProcessManager(); + auto& ppm = engine.getPostProcessManager(); + + // now emit the blurring passes + if (view.hasVsm()) { for (uint8_t layer = 0; layer < textureRequirements.layers; layer++) { - for (size_t level = 0; level < textureRequirements.levels - 1; level++) { - const bool finalize = textureRequirements.levels - 2; - shadows = ppm.vsmMipmapPass(fg, shadows, layer, level, finalize); + if (!options[layer]) continue; + const float sigma = options[layer]->vsm.blurStandardDeviation; + if (sigma > 0.0f) { + size_t kernelWidth = std::ceil(((sigma * 6.0f - 1.0f) - 5.0f) / 4.0f); + kernelWidth = kernelWidth * 4 + 5; + const float ratio = (kernelWidth + 1.0f) / sigma; + ppm.gaussianBlurPass(fg, + shadowPass->tempBlurSrc[layer], 0, + shadows, 0, layer, + false, kernelWidth, ratio); + } + } + + // If the shadow texture has more than one level, mipmapping was requested, either directly + // or indirectly via anisotropic filtering. + // So generate the mipmaps for each layer + if (textureRequirements.levels > 1) { + for (uint8_t layer = 0; layer < textureRequirements.layers; layer++) { + if (!options[layer]) continue; + for (size_t level = 0; level < textureRequirements.levels - 1; level++) { + const bool finalize = textureRequirements.levels - 2; + shadows = ppm.vsmMipmapPass(fg, shadows, layer, level, finalize); + } } } } @@ -289,7 +348,7 @@ ShadowMapManager::ShadowTechnique ShadowMapManager::updateCascadeShadowMaps( // entire camera frustum, as if we only had a single cascade. ShadowMapEntry& entry = mCascadeShadowMaps[0]; ShadowMap& map = entry.getShadowMap(); - const size_t textureDimension = entry.getLayout().size; + const size_t textureDimension = entry.getLayout().options->mapSize; const ShadowMap::ShadowMapInfo shadowMapInfo { .zResolution = mTextureZResolution, .atlasDimension = textureSize, @@ -370,7 +429,7 @@ ShadowMapManager::ShadowTechnique ShadowMapManager::updateCascadeShadowMaps( ShadowMap& shadowMap = entry.getShadowMap(); assert_invariant(entry.getLightIndex() == 0); - const size_t textureDimension = entry.getLayout().size; + const size_t textureDimension = entry.getLayout().options->mapSize; const ShadowMap::ShadowMapInfo shadowMapInfo{ .zResolution = mTextureZResolution, .atlasDimension = textureSize, @@ -435,7 +494,7 @@ ShadowMapManager::ShadowTechnique ShadowMapManager::updateSpotShadowMaps( ShadowMap& shadowMap = entry.getShadowMap(); size_t l = entry.getLightIndex(); - const size_t textureDimension = entry.getLayout().size; + const size_t textureDimension = entry.getLayout().options->mapSize; const ShadowMap::ShadowMapInfo layout{ .zResolution = mTextureZResolution, .atlasDimension = textureSize, @@ -507,44 +566,31 @@ void ShadowMapManager::calculateTextureRequirements(FEngine& engine, FView& view FScene::LightSoa& lightData) noexcept { auto& lcm = engine.getLightManager(); - auto getShadowMapSize = [&](size_t lightIndex) { + auto getShadowOptions = [&](size_t lightIndex) -> LightManager::ShadowOptions const& { FLightManager::Instance light = lightData.elementAt(lightIndex); - // The minimum size is 3 texels, as we require a 1 texel border. - return std::max(3u, lcm.getShadowMapSize(light)); - }; - - auto getShadowMapVsmSamples = [&](size_t lightIndex) { - FLightManager::Instance light = lightData.elementAt(lightIndex); - LightManager::ShadowOptions const& options = lcm.getShadowOptions(light); - return std::max((uint8_t) 1u, options.vsm.msaaSamples); + return lcm.getShadowOptions(light); }; // Lay out the shadow maps. For now, we take the largest requested dimension and allocate a // texture of that size. Each cascade / shadow map gets its own layer in the array texture. // The directional shadow cascades start on layer 0, followed by spot lights. uint8_t layer = 0; - uint16_t maxDimension = 0; + uint32_t maxDimension = 0; for (auto& cascade : mCascadeShadowMaps) { // Shadow map size should be the same for all cascades. - const size_t lightIndex = cascade.getLightIndex(); - const uint16_t dim = getShadowMapSize(lightIndex); - const uint8_t vsmSamples = getShadowMapVsmSamples(lightIndex); - maxDimension = std::max(maxDimension, dim); + auto const& options = getShadowOptions(cascade.getLightIndex()); + maxDimension = std::max(maxDimension, options.mapSize); cascade.setLayout({ - .layer = layer++, - .size = dim, - .vsmSamples = vsmSamples + .options = &options, + .layer = layer++ }); } for (auto& spotShadowMap : mSpotShadowMaps) { - const size_t lightIndex = spotShadowMap.getLightIndex(); - const uint16_t dim = getShadowMapSize(lightIndex); - const uint8_t vsmSamples = getShadowMapVsmSamples(lightIndex); - maxDimension = std::max(maxDimension, dim); + auto const& options = getShadowOptions(spotShadowMap.getLightIndex()); + maxDimension = std::max(maxDimension, options.mapSize); spotShadowMap.setLayout({ - .layer = layer++, - .size = dim, - .vsmSamples = vsmSamples + .options = &options, + .layer = layer++ }); } @@ -564,9 +610,9 @@ void ShadowMapManager::calculateTextureRequirements(FEngine& engine, FView& view } mTextureRequirements = { - maxDimension, - layersNeeded, - mipLevels + (uint16_t)maxDimension, + layersNeeded, + mipLevels }; } diff --git a/filament/src/details/ShadowMapManager.h b/filament/src/details/ShadowMapManager.h index 5360b22195..ab587838db 100644 --- a/filament/src/details/ShadowMapManager.h +++ b/filament/src/details/ShadowMapManager.h @@ -85,9 +85,8 @@ public: private: struct ShadowLayout { + LightManager::ShadowOptions const* options = nullptr; uint8_t layer = 0; - uint32_t size = 0; - uint8_t vsmSamples = 1; }; struct TextureRequirements { diff --git a/libs/filabridge/include/private/filament/UniformInterfaceBlock.h b/libs/filabridge/include/private/filament/UniformInterfaceBlock.h index fcdcecb57d..d7c852d5fa 100644 --- a/libs/filabridge/include/private/filament/UniformInterfaceBlock.h +++ b/libs/filabridge/include/private/filament/UniformInterfaceBlock.h @@ -111,6 +111,8 @@ public: // negative value if name doesn't exist or Panic if exceptions are enabled ssize_t getUniformOffset(const char* name, size_t index) const; + UniformInfo const* getUniformInfo(const char* name) const; + bool hasUniform(const char* name) const noexcept { return mInfoMap.find(name) != mInfoMap.end(); } diff --git a/libs/filabridge/src/UniformInterfaceBlock.cpp b/libs/filabridge/src/UniformInterfaceBlock.cpp index a7f86a7f1e..a848cc4619 100644 --- a/libs/filabridge/src/UniformInterfaceBlock.cpp +++ b/libs/filabridge/src/UniformInterfaceBlock.cpp @@ -116,11 +116,19 @@ UniformInterfaceBlock::UniformInterfaceBlock(Builder const& builder) noexcept } ssize_t UniformInterfaceBlock::getUniformOffset(const char* name, size_t index) const { - auto const& pos = mInfoMap.find(name); - if (!ASSERT_PRECONDITION_NON_FATAL(pos != mInfoMap.end(), "uniform named \"%s\" not found", name)) { + auto const* info = getUniformInfo(name); + if (!info) { return -1; } - return mUniformsInfoList[pos->second].getBufferOffset(index); + return info->getBufferOffset(index); +} + +UniformInterfaceBlock::UniformInfo const* UniformInterfaceBlock::getUniformInfo(const char* name) const { + auto const& pos = mInfoMap.find(name); + if (!ASSERT_PRECONDITION_NON_FATAL(pos != mInfoMap.end(), "uniform named \"%s\" not found", name)) { + return nullptr; + } + return &mUniformsInfoList[pos->second]; } diff --git a/libs/viewer/src/Settings.cpp b/libs/viewer/src/Settings.cpp index f0b56852b4..a7a1c0edab 100644 --- a/libs/viewer/src/Settings.cpp +++ b/libs/viewer/src/Settings.cpp @@ -729,6 +729,8 @@ static int parse(jsmntok_t const* tokens, int i, const char* jsonChunk, CHECK_KEY(tok); if (compare(tok, jsonChunk, "msaaSamples") == 0) { i = parse(tokens, i + 1, jsonChunk, &out->msaaSamples); + } else if (compare(tok, jsonChunk, "blurStandardDeviation") == 0) { + i = parse(tokens, i + 1, jsonChunk, &out->blurStandardDeviation); } else { slog.w << "Invalid shadow options VSM key: '" << STR(tok, jsonChunk) << "'" << io::endl; i = parse(tokens, i + 1); @@ -1168,7 +1170,8 @@ static std::ostream& operator<<(std::ostream& out, const LightManager::ShadowOpt math::float3 splitsVector = { splits[0], splits[1], splits[2] }; return out << "{\n" << "\"vsm\": {\n" - << "\"msaaSamples\": " << int(in.vsm.msaaSamples) << "\n" + << "\"msaaSamples\": " << int(in.vsm.msaaSamples) << ",\n" + << "\"blurStandardDeviation\": " << in.vsm.blurStandardDeviation << "\n" << "},\n" << "\"screenSpaceContactShadows\": " << to_string(in.screenSpaceContactShadows) << ",\n" << "\"shadowCascades\": " << int(in.shadowCascades) << ",\n" diff --git a/libs/viewer/src/SimpleViewer.cpp b/libs/viewer/src/SimpleViewer.cpp index eacc9395c9..de6a82dcd5 100644 --- a/libs/viewer/src/SimpleViewer.cpp +++ b/libs/viewer/src/SimpleViewer.cpp @@ -688,6 +688,7 @@ void SimpleViewer::updateUserInterface() { ImGui::SliderInt("VSM anisotropy", &vsmAnisotropy, 0, 3, label); mSettings.view.vsmShadowOptions.anisotropy = vsmAnisotropy; ImGui::Checkbox("VSM mipmapping", &mSettings.view.vsmShadowOptions.mipmapping); + ImGui::SliderFloat("VSM blur", &light.shadowOptions.vsm.blurStandardDeviation, 0.0, 21.0f); // These are not very useful in practice (defaults are good), but we keep them here for debugging //ImGui::SliderFloat("VSM exponent", &mSettings.view.vsmShadowOptions.exponent, 0.0, 6.0f);