diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 976e43a294..a9ad254621 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -11,6 +11,7 @@ A new header is inserted each time a *tag* is created. - gltfio: add asynchronous API to ResourceLoader. - gltfio: generate normals for flat-shaded models that do not have normals. - Material instances now allow dynamic depth testing and other rasterization state. +- Support for Bloom as a post-process effect. ## v1.4.5 diff --git a/android/filament-android/src/main/cpp/View.cpp b/android/filament-android/src/main/cpp/View.cpp index 9ce4cc89d7..b82d086cf7 100644 --- a/android/filament-android/src/main/cpp/View.cpp +++ b/android/filament-android/src/main/cpp/View.cpp @@ -246,3 +246,19 @@ Java_com_google_android_filament_View_nSetAmbientOcclusionOptions(JNIEnv*, jclas }; view->setAmbientOcclusionOptions(options); } + +extern "C" JNIEXPORT void JNICALL +Java_com_google_android_filament_View_nSetBloomOptions(JNIEnv*, jclass, + jlong nativeView, jfloat strength, jint resolution, jfloat anamorphism, jint levels, + jint blendMode, jboolean enabled) { + View* view = (View*) nativeView; + View::BloomOptions options = { + .strength = strength, + .resolution = (uint32_t)resolution, + .anamorphism = anamorphism, + .levels = (uint8_t)levels, + .blendMode = (View::BloomOptions::BlendMode)blendMode, + .enabled = (bool)enabled + }; + view->setBloomOptions(options); +} diff --git a/android/filament-android/src/main/java/com/google/android/filament/View.java b/android/filament-android/src/main/java/com/google/android/filament/View.java index 496a235817..88de60beba 100644 --- a/android/filament-android/src/main/java/com/google/android/filament/View.java +++ b/android/filament-android/src/main/java/com/google/android/filament/View.java @@ -63,6 +63,7 @@ public class View { private DynamicResolutionOptions mDynamicResolution; private RenderQuality mRenderQuality; private AmbientOcclusionOptions mAmbientOcclusionOptions; + private BloomOptions mBloomOptions; private RenderTarget mRenderTarget; /** @@ -155,6 +156,64 @@ public class View { public float intensity = 1.0f; } + /** + * Options for controlling the Bloom effect + * + * enabled: Enable or disable the bloom post-processing effect. Disabled by default. + * levels: Number of successive blurs to achieve the blur effect, the minimum is 3 and the + * maximum is 12. This value together with resolution influences the spread of the + * blur effect. This value can be silently reduced to accommodate the original + * image size. + * resolution: Resolution of bloom's minor axis. The minimum value is 2^levels and the + * the maximum is lower of the original resolution and 4096. This parameter is + * silently clamped to the minimum and maximum. + * It is highly recommended that this value be smaller than the target resolution + * after dynamic resolution is applied (horizontally and vertically). + * strength: how much of the bloom is added to the original image. Between 0 and 1. + * blendMode: Whether the bloom effect is purely additive (false) or mixed with the original + * image (true). + * anamorphism: Bloom's aspect ratio (x/y), for artistic purposes. + * + * @see setBloomOptions + */ + public static class BloomOptions { + + public enum BlendingMode { + ADD, + INTERPOLATE + } + + /** + * Strength of the bloom effect, between 0.0 and 1.0 + */ + public float strength = 0.10f; + + /** + * Resolution of minor axis (2^levels to 4096) + */ + public int resolution = 360; + + /** + * Bloom x/y aspect-ratio (1/32 to 32) + */ + public float anamorphism = 1.0f; + + /** + * Number of blur levels (3 to 12) + */ + public int levels = 6; + + /** + * How the bloom effect is applied + */ + public BlendingMode blendingMode = BlendingMode.ADD; + + /** + * enable or disable bloom + */ + public boolean enabled = false; + } + /** * Sets the quality of the HDR color buffer. * @@ -729,6 +788,31 @@ public class View { return mAmbientOcclusionOptions; } + /** + * Sets bloom options. + * + * @param options Options for bloom. + */ + public void setBloomOptions(@NonNull BloomOptions options) { + mBloomOptions = options; + nSetBloomOptions(getNativeObject(), options.strength, options.resolution, + options.anamorphism, options.levels, options.blendingMode.ordinal(), + options.enabled); + } + + /** + * Gets the bloom options + * + * @return bloom options currently set. + */ + @NonNull + public BloomOptions getBloomOptions() { + if (mBloomOptions == null) { + mBloomOptions = new BloomOptions(); + } + return mBloomOptions; + } + public long getNativeObject() { if (mNativeObject == 0) { throw new IllegalStateException("Calling method on destroyed View"); @@ -771,4 +855,5 @@ public class View { private static native void nSetAmbientOcclusion(long nativeView, int ordinal); private static native int nGetAmbientOcclusion(long nativeView); private static native void nSetAmbientOcclusionOptions(long nativeView, float radius, float bias, float power, float resolution, float intensity); + private static native void nSetBloomOptions(long nativeView, float strength, int resolution, float anamorphism, int levels, int blendMode, boolean enabled); } diff --git a/filament/CMakeLists.txt b/filament/CMakeLists.txt index 659cc27c5f..60cafb04a0 100644 --- a/filament/CMakeLists.txt +++ b/filament/CMakeLists.txt @@ -141,6 +141,8 @@ set(PRIVATE_HDRS set(MATERIAL_SRCS src/materials/defaultMaterial.mat src/materials/blit.mat + src/materials/bloomDownsample.mat + src/materials/bloomUpsample.mat src/materials/bilateralBlur.mat src/materials/mipmapDepth.mat src/materials/skybox.mat @@ -209,6 +211,7 @@ add_custom_command( DEPENDS ../shaders/src/tone_mapping.fs DEPENDS ../shaders/src/conversion_functions.fs DEPENDS ../shaders/src/dithering.fs + DEPENDS ../shaders/src/bloom.fs APPEND ) diff --git a/filament/include/filament/View.h b/filament/include/filament/View.h index de6638dad8..c6fd3306b8 100644 --- a/filament/include/filament/View.h +++ b/filament/include/filament/View.h @@ -62,6 +62,13 @@ class UTILS_PUBLIC View : public FilamentAPI { public: using TargetBufferFlags = backend::TargetBufferFlags; + enum class QualityLevel : int8_t { + LOW, + MEDIUM, + HIGH, + ULTRA + }; + /** * Dynamic resolution can be used to either reach a desired target frame rate * by lowering the resolution of a View, or to increase the quality when the @@ -115,11 +122,35 @@ public: bool homogeneousScaling = false; //!< set to true to force homogeneous scaling }; - enum class QualityLevel : int8_t { - LOW, - MEDIUM, - HIGH, - ULTRA + /** + * Options to control the bloom effect + * + * enabled: Enable or disable the bloom post-processing effect. Disabled by default. + * levels: Number of successive blurs to achieve the blur effect, the minimum is 3 and the + * maximum is 12. This value together with resolution influences the spread of the + * blur effect. This value can be silently reduced to accommodate the original + * image size. + * resolution: Resolution of bloom's minor axis. The minimum value is 2^levels and the + * the maximum is lower of the original resolution and 4096. This parameter is + * silently clamped to the minimum and maximum. + * It is highly recommended that this value be smaller than the target resolution + * after dynamic resolution is applied (horizontally and vertically). + * strength: how much of the bloom is added to the original image. Between 0 and 1. + * blendMode: Whether the bloom effect is purely additive (false) or mixed with the original + * image (true). + * anamorphism: Bloom's aspect ratio (x/y), for artistic purposes. + */ + struct BloomOptions { + enum class BlendMode : uint8_t { + ADD, //!< Bloom is modulated by the strength parameter and added to the scene + INTERPOLATE //!< Bloom is interpolated with the scene using the strength parameter + }; + float strength = 0.10f; //!< Between 0.0 and 1.0 + uint32_t resolution = 360; //!< Resolution of minor axis (2^levels to 4096) + float anamorphism = 1.0f; //!< Bloom x/y aspect-ratio (1/32 to 32) + uint8_t levels = 6; //!< number of blur levels (3 to 12) + BlendMode blendMode = BlendMode::ADD; //!< How the bloom effect is applied + bool enabled = false; //!< enable or disable bloom }; /** @@ -450,6 +481,20 @@ public: */ ToneMapping getToneMapping() const noexcept; + /** + * Enables or disables bloom in the post-processing stage. Disabled by default. + * + * @param bloom options + */ + void setBloomOptions(BloomOptions options) noexcept; + + /** + * Queries the bloom options. + * + * @return the current bloom options for this view. + */ + BloomOptions getBloomOptions() const noexcept; + /** * Enables or disables dithering in the post-processing stage. Enabled by default. * @@ -518,6 +563,7 @@ public: * Enables or disables post processing. Enabled by default. * * Post-processing includes: + * - Bloom * - Tone-mapping & gamma encoding * - Dithering * - MSAA @@ -529,7 +575,7 @@ public: * * @param enabled true enables post processing, false disables it. * - * @see setToneMapping, setAntiAliasing, setDithering, setSampleCount + * @see setBloomOptions, setToneMapping, setAntiAliasing, setDithering, setSampleCount */ void setPostProcessingEnabled(bool enabled) noexcept; diff --git a/filament/src/PostProcessManager.cpp b/filament/src/PostProcessManager.cpp index 65593e0443..8ef3eaf3c7 100644 --- a/filament/src/PostProcessManager.cpp +++ b/filament/src/PostProcessManager.cpp @@ -65,6 +65,9 @@ const uint8_t kBlueNoise[] = { 0x49, 0xbf, 0x09, 0xd2, 0x2b, 0x60, 0x07, 0x88, 0xe7, 0x50, 0x0a, 0x7c, 0xe1, 0xcf, 0x9b, 0xb7 }; +static constexpr uint8_t kMaxBloomLevels = 12u; +static_assert(kMaxBloomLevels >= 3, "We require at least 3 bloom levels"); + // ------------------------------------------------------------------------------------------------ PostProcessManager::PostProcessMaterial::PostProcessMaterial(FEngine& engine, @@ -115,6 +118,8 @@ void PostProcessManager::init() noexcept { mMipmapDepth = PostProcessMaterial(mEngine, MATERIALS_MIPMAPDEPTH_DATA, MATERIALS_MIPMAPDEPTH_SIZE); mBilateralBlur = PostProcessMaterial(mEngine, MATERIALS_BILATERALBLUR_DATA, MATERIALS_BILATERALBLUR_SIZE); mSeparableGaussianBlur = PostProcessMaterial(mEngine, MATERIALS_SEPARABLEGAUSSIANBLUR_DATA, MATERIALS_SEPARABLEGAUSSIANBLUR_SIZE); + mBloomDownsample = PostProcessMaterial(mEngine, MATERIALS_BLOOMDOWNSAMPLE_DATA, MATERIALS_BLOOMDOWNSAMPLE_SIZE); + mBloomUpsample = PostProcessMaterial(mEngine, MATERIALS_BLOOMUPSAMPLE_DATA, MATERIALS_BLOOMUPSAMPLE_SIZE); mBlit = PostProcessMaterial(mEngine, MATERIALS_BLIT_DATA, MATERIALS_BLIT_SIZE); mTonemapping = PostProcessMaterial(mEngine, MATERIALS_TONEMAPPING_DATA, MATERIALS_TONEMAPPING_SIZE); mFxaa = PostProcessMaterial(mEngine, MATERIALS_FXAA_DATA, MATERIALS_FXAA_SIZE); @@ -169,20 +174,24 @@ void PostProcessManager::init() noexcept { void PostProcessManager::terminate(DriverApi& driver) noexcept { driver.destroyTexture(mNoSSAOTexture); driver.destroyTexture(mNoiseTexture); - mSSAO.terminate(mEngine); - mMipmapDepth.terminate(mEngine); - mBilateralBlur.terminate(mEngine); - mSeparableGaussianBlur.terminate(mEngine); - mBlit.terminate(mEngine); - mTonemapping.terminate(mEngine); - mFxaa.terminate(mEngine); + FEngine& engine = mEngine; + mSSAO.terminate(engine); + mMipmapDepth.terminate(engine); + mBilateralBlur.terminate(engine); + mSeparableGaussianBlur.terminate(engine); + mBloomDownsample.terminate(engine); + mBloomUpsample.terminate(engine); + mBlit.terminate(engine); + mTonemapping.terminate(engine); + mFxaa.terminate(engine); } // ------------------------------------------------------------------------------------------------ -FrameGraphId PostProcessManager::toneMapping(FrameGraph& fg, - FrameGraphId input, TextureFormat outFormat, - bool dithering, bool translucent, bool fxaa) noexcept { +FrameGraphId PostProcessManager::toneMapping(FrameGraph& fg, + FrameGraphId input, + backend::TextureFormat outFormat, bool translucent, bool fxaa, float2 scale, + View::BloomOptions bloomOptions, bool dithering) noexcept { FEngine& engine = mEngine; Handle const& fullScreenRenderPrimitive = engine.getFullScreenRenderPrimitive(); @@ -190,9 +199,18 @@ FrameGraphId PostProcessManager::toneMapping(FrameGraph& fg, struct PostProcessToneMapping { FrameGraphId input; FrameGraphId output; + FrameGraphId bloom; FrameGraphRenderTargetHandle rt; }; + FrameGraphId bloomBlur; + + float bloom = 0.0f; + if (bloomOptions.enabled) { + bloom = clamp(bloomOptions.strength, 0.0f, 1.0f); + bloomBlur = bloomPass(fg, input, TextureFormat::R11F_G11F_B10F, bloomOptions, scale); + } + auto& ppToneMapping = fg.addPass("tonemapping", [&](FrameGraph::Builder& builder, PostProcessToneMapping& data) { auto const& inputDesc = fg.getDescriptor(input); @@ -203,14 +221,32 @@ FrameGraphId PostProcessManager::toneMapping(FrameGraph& fg, .format = outFormat }); data.rt = builder.createRenderTarget(data.output); + + if (!bloomBlur.isValid()) { + // we need a dummy texture + bloomBlur = builder.createTexture("dummy", {}); + } + data.bloom = builder.sample(bloomBlur); }, [=](FrameGraphPassResources const& resources, PostProcessToneMapping const& data, DriverApi& driver) { - auto const& color = resources.getTexture(data.input); + auto const& colorTexture = resources.getTexture(data.input); + auto const& bloomTexture = resources.getTexture(data.bloom); FMaterialInstance* pInstance = mTonemapping.getMaterialInstance(); - pInstance->setParameter("colorBuffer", color, {}); + pInstance->setParameter("colorBuffer", colorTexture, { /* shader uses texelFetch */ }); + pInstance->setParameter("bloomBuffer", bloomTexture, { + .filterMag = SamplerMagFilter::LINEAR, + .filterMin = SamplerMinFilter::LINEAR /* always read base level in shader */ + }); + + float2 bloomParameter{ bloom / float(bloomOptions.levels), 1.0f }; + if (bloomOptions.blendMode == View::BloomOptions::BlendMode::INTERPOLATE) { + bloomParameter.y = 1.0f - bloomParameter.x; + } + pInstance->setParameter("dithering", dithering); + pInstance->setParameter("bloom", bloomParameter); pInstance->setParameter("fxaa", fxaa); pInstance->commit(driver); @@ -765,7 +801,7 @@ FrameGraphId PostProcessManager::gaussianBlurPass(FrameGraph& const size_t kernelStorageSize = mSeparableGaussianBlurKernelStorageSize; auto& gaussianBlurPasses = fg.addPass("Gaussian Blur Passes", - [&](FrameGraph::Builder& builder, BlurPassData& data) { + [&](FrameGraph::Builder& builder, auto& data) { auto desc = builder.getDescriptor(input); if (!output.isValid()) { @@ -795,15 +831,16 @@ FrameGraphId PostProcessManager::gaussianBlurPass(FrameGraph& }, TargetBufferFlags::NONE); }, [=](FrameGraphPassResources const& resources, - BlurPassData const& data, DriverApi& driver) { + auto const& data, DriverApi& driver) { PostProcessMaterial const& separableGaussianBlur = mSeparableGaussianBlur; FMaterialInstance* const mi = separableGaussianBlur.getMaterialInstance(); - PipelineState pipeline; - pipeline.program = separableGaussianBlur.getProgram(); - pipeline.rasterState = separableGaussianBlur.getMaterial()->getRasterState(); - pipeline.scissor = mi->getScissor(); + PipelineState pipeline{ + .program = separableGaussianBlur.getProgram(), + .rasterState = separableGaussianBlur.getMaterial()->getRasterState(), + .scissor = mi->getScissor() + }; float2 kernel[64]; size_t m = computeGaussianCoefficients(kernel, @@ -864,4 +901,140 @@ FrameGraphId PostProcessManager::gaussianBlurPass(FrameGraph& return gaussianBlurPasses.getData().out; } +FrameGraphId PostProcessManager::bloomPass(FrameGraph& fg, + FrameGraphId input, backend::TextureFormat outFormat, + View::BloomOptions& bloomOptions, float2 scale) noexcept { + + Handle fullScreenRenderPrimitive = mEngine.getFullScreenRenderPrimitive(); + + // Figure out a good size for the bloom buffer. We pick the major axis lower + // power of two, and scale the minor axis accordingly. + auto const& desc = fg.getDescriptor(input); + uint32_t width = desc.width / scale.x; + uint32_t height = desc.height / scale.y; + if (bloomOptions.anamorphism >= 1.0) { + height *= bloomOptions.anamorphism; + } else if (bloomOptions.anamorphism < 1.0) { + width *= 1.0f / std::max(bloomOptions.anamorphism, 1.0f / 4096.0f); + } + // FIXME: compensate for dynamic scaling + uint32_t& major = width > height ? width : height; + uint32_t& minor = width < height ? width : height; + uint32_t newMinor = clamp(bloomOptions.resolution, + 1u << bloomOptions.levels, std::min(minor, 1u << kMaxBloomLevels)); + major = major * uint64_t(newMinor) / minor; + minor = newMinor; + + // we might need to adjust the max # of levels + const uint8_t maxLevels = static_cast(std::ilogbf(major) + 1); + bloomOptions.levels = std::min(bloomOptions.levels, maxLevels); + bloomOptions.levels = std::min(bloomOptions.levels, kMaxBloomLevels); + +// slog.d << desc.width << "x" << desc.height << " -> " << width << "x" << height +// << ", levels=" << +bloomOptions.levels << io::endl; + + struct BloomPassData { + FrameGraphId in; + FrameGraphId out; + FrameGraphRenderTargetHandle outRT[kMaxBloomLevels]; + }; + + auto& bloomPass = fg.addPass("Gaussian Blur Passes", + [&](FrameGraph::Builder& builder, auto& data) { + data.in = builder.sample(input); + data.out = builder.createTexture("Bloom Texture", { + .width = width, + .height = height, + .levels = bloomOptions.levels, + .format = outFormat + }); + data.out = builder.write(builder.sample(data.out)); + + for (size_t i = 0; i < bloomOptions.levels; i++) { + data.outRT[i] = builder.createRenderTarget("Bloom target", { + .attachments = {{ data.out, uint8_t(i) }, {}} + }, TargetBufferFlags::NONE); + } + }, + [=](FrameGraphPassResources const& resources, + auto const& data, DriverApi& driver) { + + PostProcessMaterial const& bloomDownsample = mBloomDownsample; + FMaterialInstance* mi = bloomDownsample.getMaterialInstance(); + + PipelineState pipeline{ + .program = bloomDownsample.getProgram(), + .rasterState = bloomDownsample.getMaterial()->getRasterState(), + .scissor = mi->getScissor(), + }; + + auto hwIn = resources.getTexture(data.in); + auto hwOut = resources.getTexture(data.out); + auto const& outDesc = resources.getDescriptor(data.out); + + mi->use(driver); + mi->setParameter("source", hwIn, { + .filterMag = SamplerMagFilter::LINEAR, + .filterMin = SamplerMinFilter::LINEAR /* level is always 0 */ + }); + mi->setParameter("level", 0.0f); + + // downsample phase + for (size_t i = 0; i < bloomOptions.levels; i++) { + auto hwOutRT = resources.getRenderTarget(data.outRT[i]); + + auto w = FTexture::valueForLevel(i, outDesc.width); + auto h = FTexture::valueForLevel(i, outDesc.height); + mi->setParameter("resolution", float4{ w, h, 1.0f / w, 1.0f / h }); + mi->commit(driver); + + hwOutRT.params.flags.discardStart = TargetBufferFlags::COLOR; + hwOutRT.params.flags.discardEnd = TargetBufferFlags::NONE; + driver.beginRenderPass(hwOutRT.target, hwOutRT.params); + driver.draw(pipeline, fullScreenRenderPrimitive); + driver.endRenderPass(); + + // prepare the next level + mi->setParameter("source", hwOut, { + .filterMag = SamplerMagFilter::LINEAR, + .filterMin = SamplerMinFilter::LINEAR_MIPMAP_NEAREST + }); + mi->setParameter("level", float(i)); + } + + // upsample phase + PostProcessMaterial const& bloomUpsample = mBloomUpsample; + mi = bloomUpsample.getMaterialInstance(); + pipeline.program = bloomUpsample.getProgram(); + pipeline.rasterState = bloomUpsample.getMaterial()->getRasterState(); + pipeline.scissor = mi->getScissor(); + pipeline.rasterState.blendFunctionSrcRGB = BlendFunction::ONE; + pipeline.rasterState.blendFunctionDstRGB = BlendFunction::ONE; + + mi->use(driver); + + for (size_t i = bloomOptions.levels - 1; i >= 1; i--) { + auto hwDstRT = resources.getRenderTarget(data.outRT[i - 1]); + hwDstRT.params.flags.discardStart = TargetBufferFlags::NONE; // because we'll blend + hwDstRT.params.flags.discardEnd = TargetBufferFlags::NONE; + + auto w = FTexture::valueForLevel(i - 1, outDesc.width); + auto h = FTexture::valueForLevel(i - 1, outDesc.height); + mi->setParameter("resolution", float4{ w, h, 1.0f / w, 1.0f / h }); + mi->setParameter("source", hwOut, { + .filterMag = SamplerMagFilter::LINEAR, + .filterMin = SamplerMinFilter::LINEAR_MIPMAP_NEAREST + }); + mi->setParameter("level", float(i)); + mi->commit(driver); + + driver.beginRenderPass(hwDstRT.target, hwDstRT.params); + driver.draw(pipeline, fullScreenRenderPrimitive); + driver.endRenderPass(); + } + }); + + return bloomPass.getData().out; +} + } // namespace filament diff --git a/filament/src/PostProcessManager.h b/filament/src/PostProcessManager.h index 93013c538f..096acf5d3b 100644 --- a/filament/src/PostProcessManager.h +++ b/filament/src/PostProcessManager.h @@ -44,9 +44,10 @@ public: void init() noexcept; void terminate(backend::DriverApi& driver) noexcept; - FrameGraphId toneMapping(FrameGraph& fg, - FrameGraphId input, - backend::TextureFormat outFormat, bool dithering, bool translucent, bool fxaa) noexcept; + FrameGraphId toneMapping(FrameGraph& fg, + FrameGraphId input, + backend::TextureFormat outFormat, bool translucent, bool fxaa, math::float2 scale, + View::BloomOptions bloomOptions, bool dithering) noexcept; FrameGraphId fxaa(FrameGraph& fg, FrameGraphId input, backend::TextureFormat outFormat, @@ -94,6 +95,11 @@ private: FrameGraphId input, FrameGraphId depth, math::int2 axis) noexcept; + FrameGraphId bloomPass(FrameGraph& fg, + FrameGraphId input, backend::TextureFormat outFormat, + View::BloomOptions& bloomOptions, math::float2 scale) noexcept; + + class PostProcessMaterial { public: PostProcessMaterial() noexcept = default; @@ -123,6 +129,8 @@ private: PostProcessMaterial mMipmapDepth; PostProcessMaterial mBilateralBlur; PostProcessMaterial mSeparableGaussianBlur; + PostProcessMaterial mBloomDownsample; + PostProcessMaterial mBloomUpsample; PostProcessMaterial mBlit; PostProcessMaterial mTonemapping; PostProcessMaterial mFxaa; diff --git a/filament/src/Renderer.cpp b/filament/src/Renderer.cpp index b2466e6431..acdaa27455 100644 --- a/filament/src/Renderer.cpp +++ b/filament/src/Renderer.cpp @@ -379,7 +379,8 @@ void FRenderer::renderJob(ArenaScope& arena, FView& view) { if (hasPostProcess) { if (toneMapping) { - input = ppm.toneMapping(fg, input, ldrFormat, dithering, translucent, fxaa); + input = ppm.toneMapping(fg, input, ldrFormat, translucent, fxaa, scale, + view.getBloomOptions(), dithering); } if (fxaa) { input = ppm.fxaa(fg, input, ldrFormat, !toneMapping || translucent); diff --git a/filament/src/View.cpp b/filament/src/View.cpp index 1460d3f5cd..e43395cdaf 100644 --- a/filament/src/View.cpp +++ b/filament/src/View.cpp @@ -102,6 +102,9 @@ void FView::terminate(FEngine& engine) { } void FView::setViewport(filament::Viewport const& viewport) noexcept { + // catch the cases were user had an underflow and didn't catch it. + assert((int32_t)viewport.width > 0); + assert((int32_t)viewport.height > 0); mViewport = viewport; } @@ -972,5 +975,13 @@ View::AmbientOcclusionOptions const& View::getAmbientOcclusionOptions() const no return upcast(this)->getAmbientOcclusionOptions(); } +void View::setBloomOptions(View::BloomOptions options) noexcept { + upcast(this)->setBloomOptions(options); +} + +View::BloomOptions View::getBloomOptions() const noexcept { + return upcast(this)->getBloomOptions(); +} + } // namespace filament diff --git a/filament/src/details/View.h b/filament/src/details/View.h index 4ebd1f39db..613894be87 100644 --- a/filament/src/details/View.h +++ b/filament/src/details/View.h @@ -243,6 +243,15 @@ public: return mAmbientOcclusionOptions; } + void setBloomOptions(BloomOptions options) noexcept { + options.levels = math::clamp(options.levels, uint8_t(3), uint8_t(12)); + mBloomOptions = options; + } + + BloomOptions getBloomOptions() const noexcept { + return mBloomOptions; + } + Range const& getVisibleRenderables() const noexcept { return mVisibleRenderables; } @@ -339,6 +348,7 @@ private: bool mHasPostProcessPass = true; AmbientOcclusion mAmbientOcclusion = AmbientOcclusion::NONE; AmbientOcclusionOptions mAmbientOcclusionOptions{}; + BloomOptions mBloomOptions; using duration = std::chrono::duration; DynamicResolutionOptions mDynamicResolution; diff --git a/filament/src/materials/bloomDownsample.mat b/filament/src/materials/bloomDownsample.mat new file mode 100644 index 0000000000..071e9988b6 --- /dev/null +++ b/filament/src/materials/bloomDownsample.mat @@ -0,0 +1,90 @@ +material { + name : bloomDownsample, + parameters : [ + { + type : sampler2d, + name : source, + precision: medium + }, + { + type : float4, + name : resolution, + precision: high + }, + { + type : float, + name : level + } + ], + variables : [ + vertex + ], + domain : postprocess, + depthWrite : false, + depthCulling : false +} + +vertex { + void postProcessVertex(inout PostProcessVertexInputs postProcess) { + postProcess.vertex.xy = postProcess.normalizedUV; + } +} + +fragment { + vec3 box4x4(vec3 s0, vec3 s1, vec3 s2, vec3 s3) { + return (s0 + s1 + s2 + s3) * 0.25; + } + + vec3 box4x4Reinhard(vec3 s0, vec3 s1, vec3 s2, vec3 s3) { + float w0 = 1.0 / (1.0 + max3(s0)); + float w1 = 1.0 / (1.0 + max3(s1)); + float w2 = 1.0 / (1.0 + max3(s2)); + float w3 = 1.0 / (1.0 + max3(s3)); + return (s0 * w0 + s1 * w1 + s2 * w2 + s3 * w3) * (1.0 / (w0 + w1 + w2 + w3)); + } + + void postProcess(inout PostProcessInputs postProcess) { + float lod = materialParams.level; + highp vec2 uv = variable_vertex.xy; + highp float du = materialParams.resolution.z; + highp float dv = materialParams.resolution.w; + + vec3 c = textureLod(materialParams_source, uv, lod).rgb; + + vec3 lt = textureLod(materialParams_source, uv + 0.5 * vec2(-du, -dv), lod).rgb; + vec3 rt = textureLod(materialParams_source, uv + 0.5 * vec2( du, -dv), lod).rgb; + vec3 rb = textureLod(materialParams_source, uv + 0.5 * vec2( du, dv), lod).rgb; + vec3 lb = textureLod(materialParams_source, uv + 0.5 * vec2(-du, dv), lod).rgb; + + vec3 lt2 = textureLod(materialParams_source, uv + vec2(-du, -dv), lod).rgb; + vec3 rt2 = textureLod(materialParams_source, uv + vec2( du, -dv), lod).rgb; + vec3 rb2 = textureLod(materialParams_source, uv + vec2( du, dv), lod).rgb; + vec3 lb2 = textureLod(materialParams_source, uv + vec2(-du, dv), lod).rgb; + + vec3 l = textureLod(materialParams_source, uv + vec2(-du, 0.0), lod).rgb; + vec3 t = textureLod(materialParams_source, uv + vec2( 0.0, -dv), lod).rgb; + vec3 r = textureLod(materialParams_source, uv + vec2( du, 0.0), lod).rgb; + vec3 b = textureLod(materialParams_source, uv + vec2( 0.0, dv), lod).rgb; + + // five h4x4 boxes + vec3 c0, c1; + if (materialParams.level > 0.5) { + // common case + c0 = box4x4(lt, rt, rb, lb); + c1 = box4x4(c, l, t, lt2); + c1 += box4x4(c, r, t, rt2); + c1 += box4x4(c, r, b, rb2); + c1 += box4x4(c, l, b, lb2); + } else { + // only first level downsampling + c0 = box4x4Reinhard(lt, rt, rb, lb); + c1 = box4x4Reinhard(c, l, t, lt2); + c1 += box4x4Reinhard(c, r, t, rt2); + c1 += box4x4Reinhard(c, r, b, rb2); + c1 += box4x4Reinhard(c, l, b, lb2); + } + + // weighted average of the five boxes + postProcess.color.rgb = c0 * 0.5 + c1 * 0.125; + } +} diff --git a/filament/src/materials/bloomUpsample.mat b/filament/src/materials/bloomUpsample.mat new file mode 100644 index 0000000000..3f4ff59762 --- /dev/null +++ b/filament/src/materials/bloomUpsample.mat @@ -0,0 +1,53 @@ +material { + name : bloomUpsample, + parameters : [ + { + type : sampler2d, + name : source, + precision: medium + }, + { + type : float4, + name : resolution, + precision: high + }, + { + type : float, + name : level + } + ], + variables : [ + vertex + ], + domain : postprocess, + depthWrite : false, + depthCulling : false +} + +vertex { + void postProcessVertex(inout PostProcessVertexInputs postProcess) { + postProcess.vertex.xy = postProcess.normalizedUV; + } +} + +fragment { + void postProcess(inout PostProcessInputs postProcess) { + float lod = materialParams.level; + highp vec2 uv = variable_vertex.xy; + highp float du = 2.0 * materialParams.resolution.z; + highp float dv = 2.0 * materialParams.resolution.w; + + vec3 c0 = 4.0 * textureLod(materialParams_source, uv, lod).rgb; + c0 += textureLod(materialParams_source, uv + vec2(-du, -dv), lod).rgb; + c0 += textureLod(materialParams_source, uv + vec2( du, -dv), lod).rgb; + c0 += textureLod(materialParams_source, uv + vec2( du, dv), lod).rgb; + c0 += textureLod(materialParams_source, uv + vec2(-du, dv), lod).rgb; + + vec3 c1 = textureLod(materialParams_source, uv + vec2(-du, 0.0), lod).rgb; + c1 += textureLod(materialParams_source, uv + vec2( 0.0, -dv), lod).rgb; + c1 += textureLod(materialParams_source, uv + vec2( du, 0.0), lod).rgb; + c1 += textureLod(materialParams_source, uv + vec2( 0.0, dv), lod).rgb; + + postProcess.color.rgb = (c0 + 2.0 * c1) * (1.0 / 16.0); + } +} diff --git a/filament/src/materials/tonemapping.mat b/filament/src/materials/tonemapping.mat index d643c804dc..9b5762f477 100644 --- a/filament/src/materials/tonemapping.mat +++ b/filament/src/materials/tonemapping.mat @@ -6,6 +6,11 @@ material { name : colorBuffer, precision: high }, + { + type : sampler2d, + name : bloomBuffer, + precision: medium + }, { type : int, name : dithering @@ -13,18 +18,32 @@ material { { type : int, name : fxaa + }, + { + type : float2, + name : bloom } ], + variables : [ + vertex + ], depthWrite : false, depthCulling : false, domain: postprocess } +vertex { + void postProcessVertex(inout PostProcessVertexInputs postProcess) { + postProcess.vertex.xy = postProcess.normalizedUV; + } +} + fragment { #include "../../../shaders/src/tone_mapping.fs" #include "../../../shaders/src/conversion_functions.fs" #include "../../../shaders/src/dithering.fs" +#include "../../../shaders/src/bloom.fs" vec3 resolveFragment(const ivec2 uv) { return texelFetch(materialParams_colorBuffer, uv, 0).rgb; @@ -37,6 +56,9 @@ fragment { vec4 resolve() { #if POST_PROCESS_OPAQUE vec4 color = vec4(resolveFragment(ivec2(getUV())), 1.0); + if (materialParams.bloom.x > 0.0) { + color.rgb = bloom(color.rgb); + } color.rgb = tonemap(color.rgb); color.rgb = OECF(color.rgb); if (materialParams.fxaa > 0) { @@ -45,6 +67,9 @@ fragment { #else vec4 color = resolveAlphaFragment(ivec2(getUV())); color.rgb /= color.a + FLT_EPS; + if (materialParams.bloom.x > 0.0) { + color.rgb = bloom(color.rgb); + } color.rgb = tonemap(color.rgb); color.rgb = OECF(color.rgb); color.rgb *= color.a + FLT_EPS; diff --git a/libs/gltfio/include/gltfio/SimpleViewer.h b/libs/gltfio/include/gltfio/SimpleViewer.h index 9ea746ca37..34aea18dd0 100644 --- a/libs/gltfio/include/gltfio/SimpleViewer.h +++ b/libs/gltfio/include/gltfio/SimpleViewer.h @@ -191,6 +191,7 @@ private: bool mEnableFxaa = true; bool mEnableMsaa = true; bool mEnableSsao = true; + filament::View::BloomOptions mBloomOptions = { .enabled = true }; int mSidebarWidth; uint32_t mFlags; }; @@ -425,6 +426,7 @@ void SimpleViewer::updateUserInterface() { ImGui::Checkbox("FXAA", &mEnableFxaa); ImGui::Checkbox("MSAA 4x", &mEnableMsaa); ImGui::Checkbox("SSAO", &mEnableSsao); + ImGui::Checkbox("Bloom", &mBloomOptions.enabled); } mView->setDithering(mEnableDithering ? View::Dithering::TEMPORAL : View::Dithering::NONE); @@ -432,6 +434,7 @@ void SimpleViewer::updateUserInterface() { mView->setSampleCount(mEnableMsaa ? 4 : 1); mView->setAmbientOcclusion( mEnableSsao ? View::AmbientOcclusion::SSAO : View::AmbientOcclusion::NONE); + mView->setBloomOptions(mBloomOptions); if (ImGui::CollapsingHeader("Light", headerFlags)) { ImGui::SliderFloat("IBL intensity", &mIblIntensity, 0.0f, 100000.0f); diff --git a/samples/material_sandbox.cpp b/samples/material_sandbox.cpp index 6bdb709992..c68cd2e4ab 100644 --- a/samples/material_sandbox.cpp +++ b/samples/material_sandbox.cpp @@ -398,6 +398,10 @@ static void gui(filament::Engine* engine, filament::View*) { ImGui::Checkbox("msaa 4x", ¶ms.msaa); ImGui::Checkbox("tone-mapping", ¶ms.tonemapping); ImGui::Indent(); + ImGui::Checkbox("bloom", ¶ms.bloomOptions.enabled); + if (params.bloomOptions.enabled) { + ImGui::SliderFloat("strength", ¶ms.bloomOptions.strength, 0.0f, 1.0f); + } ImGui::Checkbox("dithering", ¶ms.dithering); ImGui::Unindent(); ImGui::Checkbox("fxaa", ¶ms.fxaa); @@ -487,6 +491,7 @@ static void preRender(filament::Engine*, filament::View* view, filament::Scene*, view->setAntiAliasing(g_params.fxaa ? View::AntiAliasing::FXAA : View::AntiAliasing::NONE); view->setToneMapping(g_params.tonemapping ? View::ToneMapping::ACES : View::ToneMapping::LINEAR); view->setDithering(g_params.dithering ? View::Dithering::TEMPORAL : View::Dithering::NONE); + view->setBloomOptions(g_params.bloomOptions); view->setSampleCount((uint8_t) (g_params.msaa ? 4 : 1)); view->setAmbientOcclusion( g_params.ssao ? View::AmbientOcclusion::SSAO : View::AmbientOcclusion::NONE); @@ -510,6 +515,8 @@ int main(int argc, char* argv[]) { g_filenames.push_back(filename); } + g_params.bloomOptions.enabled = true; + g_config.title = "Material Sandbox"; FilamentApp& filamentApp = FilamentApp::get(); filamentApp.run(g_config, setup, cleanup, gui, preRender); diff --git a/samples/material_sandbox.h b/samples/material_sandbox.h index 5f4932358b..ee0b372073 100644 --- a/samples/material_sandbox.h +++ b/samples/material_sandbox.h @@ -108,6 +108,7 @@ struct SandboxParameters { float polygonOffsetSlope = 2.0; bool ssao = false; filament::View::AmbientOcclusionOptions ssaoOptions; + filament::View::BloomOptions bloomOptions; }; inline void createInstances(SandboxParameters& params, filament::Engine& engine) { diff --git a/shaders/src/bloom.fs b/shaders/src/bloom.fs new file mode 100644 index 0000000000..e44367a5df --- /dev/null +++ b/shaders/src/bloom.fs @@ -0,0 +1,9 @@ +//------------------------------------------------------------------------------ +// Bloom +//------------------------------------------------------------------------------ + +vec3 bloom(const vec3 color) { + highp vec2 uv = variable_vertex.xy; + vec3 blurred = textureLod(materialParams_bloomBuffer, uv, 0.0).rgb; + return blurred * materialParams.bloom.x + color * materialParams.bloom.y; +}