Improve bloom

- add a quality option
- remove the ping-pong code, we'll disable for GPU that don't work
  instead.
- improve quality by doing a better first downscale
  (using a 5x5 gaussian).
- improve performance by using a 9 tap filter instead of 13 for
  in most cases
- fix usages of setMinMaxLevels as it resets the base level to "min"
This commit is contained in:
Mathias Agopian
2023-08-24 21:53:43 -07:00
committed by Mathias Agopian
parent 9c0fb67b87
commit 9f62dc2f2f
18 changed files with 397 additions and 245 deletions

View File

@@ -1418,6 +1418,17 @@ public class View {
* limit highlights to this value before bloom [10, +inf]
*/
public float highlight = 1000.0f;
/**
* Bloom quality level.
* LOW (default): use a more optimized down-sampling filter, however there can be artifacts
* with dynamic resolution, this can be alleviated by using the homogenous mode.
* MEDIUM: Good balance between quality and performance.
* HIGH: In this mode the bloom resolution is automatically increased to avoid artifacts.
* This mode can be significantly slower on mobile, especially at high resolution.
* This mode greatly improves the anamorphic bloom.
*/
@NonNull
public QualityLevel quality = QualityLevel.LOW;
/**
* enable screen-space lens flare
*/

View File

@@ -230,6 +230,8 @@ set(MATERIAL_SRCS
src/materials/flare/flare.mat
src/materials/blitLow.mat
src/materials/bloom/bloomDownsample.mat
src/materials/bloom/bloomDownsample2x.mat
src/materials/bloom/bloomDownsample9.mat
src/materials/bloom/bloomUpsample.mat
src/materials/ssao/bilateralBlur.mat
src/materials/ssao/bilateralBlurBentNormals.mat

View File

@@ -141,6 +141,17 @@ struct BloomOptions {
bool enabled = false; //!< enable or disable bloom
float highlight = 1000.0f; //!< limit highlights to this value before bloom [10, +inf]
/**
* Bloom quality level.
* LOW (default): use a more optimized down-sampling filter, however there can be artifacts
* with dynamic resolution, this can be alleviated by using the homogenous mode.
* MEDIUM: Good balance between quality and performance.
* HIGH: In this mode the bloom resolution is automatically increased to avoid artifacts.
* This mode can be significantly slower on mobile, especially at high resolution.
* This mode greatly improves the anamorphic bloom.
*/
QualityLevel quality = QualityLevel::LOW;
bool lensFlare = false; //!< enable screen-space lens flare
bool starburst = true; //!< enable starburst effect on lens flare
float chromaticAberration = 0.005f; //!< amount of chromatic aberration

View File

@@ -212,6 +212,8 @@ static const PostProcessManager::MaterialInfo sMaterialList[] = {
{ "bilateralBlurBentNormals", MATERIAL(BILATERALBLURBENTNORMALS) },
{ "blitLow", MATERIAL(BLITLOW) },
{ "bloomDownsample", MATERIAL(BLOOMDOWNSAMPLE) },
{ "bloomDownsample2x", MATERIAL(BLOOMDOWNSAMPLE2X) },
{ "bloomDownsample9", MATERIAL(BLOOMDOWNSAMPLE9) },
{ "bloomUpsample", MATERIAL(BLOOMUPSAMPLE) },
{ "colorGrading", MATERIAL(COLORGRADING) },
{ "colorGradingAsSubpass", MATERIAL(COLORGRADINGASSUBPASS) },
@@ -450,7 +452,6 @@ PostProcessManager::StructurePassOutput PostProcessManager::structure(FrameGraph
for (size_t level = 0; level < levelCount - 1; level++) {
auto out = resources.getRenderPassInfo(level);
driver.setMinMaxLevels(in, level, level);
mi->setParameter("level", uint32_t(level));
commitAndRender(out, material, driver);
}
driver.setMinMaxLevels(in, 0, levelCount - 1);
@@ -1596,8 +1597,7 @@ FrameGraphId<FrameGraphTexture> PostProcessManager::dof(FrameGraph& fg,
auto const& out = resources.getRenderPassInfo(data.rp[level]);
driver.setMinMaxLevels(inOutColor, level, level);
driver.setMinMaxLevels(inOutCoc, level, level);
mi->setParameter("mip", uint32_t(level));
mi->setParameter("weightScale", 0.5f / float(1u<<level)); // FIXME: halfres?
mi->setParameter("weightScale", 0.5f / float(1u << level)); // FIXME: halfres?
mi->setParameter("texelSize", float2{ 1.0f / w, 1.0f / h });
mi->commit(driver);
render(out, pipeline, driver);
@@ -1870,15 +1870,53 @@ PostProcessManager::BloomPassOutput PostProcessManager::bloom(FrameGraph& fg,
return bloomPass(fg, input, outFormat, inoutBloomOptions, scale);
}
FrameGraphId<FrameGraphTexture> PostProcessManager::downscalePass(FrameGraph& fg,
FrameGraphId<FrameGraphTexture> input,
FrameGraphTexture::Descriptor const& outDesc,
bool threshold, float highlight, bool fireflies) noexcept {
struct DownsampleData {
FrameGraphId<FrameGraphTexture> input;
FrameGraphId<FrameGraphTexture> output;
};
auto& downsamplePass = fg.addPass<DownsampleData>("Downsample",
[&](FrameGraph::Builder& builder, auto& data) {
data.input = builder.sample(input);
data.output = builder.createTexture("Downsample-output", outDesc);
builder.declareRenderPass(data.output);
},
[=](FrameGraphResources const& resources,
auto const& data, DriverApi& driver) {
auto const& material = getPostProcessMaterial("bloomDownsample2x");
auto* mi = material.getMaterialInstance(mEngine);
mi->setParameter("source", resources.getTexture(data.input), {
.filterMag = SamplerMagFilter::LINEAR,
.filterMin = SamplerMinFilter::LINEAR
});
mi->setParameter("level", 0);
mi->setParameter("threshold", threshold ? 1.0f : 0.0f);
mi->setParameter("fireflies", fireflies ? 1.0f : 0.0f);
mi->setParameter("invHighlight", std::isinf(highlight) ? 0.0f : 1.0f / highlight);
commitAndRender(resources.getRenderPassInfo(), material, driver);
});
return downsamplePass->output;
}
PostProcessManager::BloomPassOutput PostProcessManager::bloomPass(FrameGraph& fg,
FrameGraphId<FrameGraphTexture> input, TextureFormat outFormat,
BloomOptions& inoutBloomOptions, float2 scale) noexcept {
// Figure out a good size for the bloom buffer.
auto const& desc = fg.getDescriptor(input);
// Figure out a good size for the bloom buffer. We must use a fixed bloom buffer size so
// that the size/strength of the bloom doesn't vary much with the resolution, otherwise
// dynamic resolution would affect the bloom effect too much.
auto desc = fg.getDescriptor(input);
// width and height after dynamic resolution upscaling
const float aspect = (float(desc.width) * scale.y) / (float(desc.height) * scale.x);
// FIXME: don't allow inoutBloomOptions.resolution to be larger than input's resolution
// (avoid upscale) but how does this affect dynamic resolution
// FIXME: check what happens on WebGL and intel's processors
// compute the desired bloom buffer size
float bloomHeight = float(inoutBloomOptions.resolution);
float bloomWidth = bloomHeight * aspect;
@@ -1893,38 +1931,73 @@ PostProcessManager::BloomPassOutput PostProcessManager::bloomPass(FrameGraph& fg
bloomHeight *= inoutBloomOptions.anamorphism;
}
// convert back to integer width/height
const uint32_t width = std::max(1u, uint32_t(std::floor(bloomWidth)));
const uint32_t height = std::max(1u, uint32_t(std::floor(bloomHeight)));
// we might need to adjust the max # of levels
const uint32_t major = uint32_t(std::max(bloomWidth, bloomHeight));
const uint8_t maxLevels = FTexture::maxLevelCount(major);
inoutBloomOptions.levels = std::min(inoutBloomOptions.levels, maxLevels);
inoutBloomOptions.levels = std::min(inoutBloomOptions.levels, kMaxBloomLevels);
if (2 * width < desc.width || 2 * height < desc.height) {
// if we're scaling down by more than 2x, prescale the image with a blit to improve
// performance. This is important on mobile/tilers.
input = opaqueBlit(fg, input, { 0, 0, desc.width, desc.height }, {
.width = std::max(1u, desc.width / 2),
.height = std::max(1u, desc.height / 2),
.format = outFormat
});
if (inoutBloomOptions.quality == QualityLevel::LOW) {
// In low quality mode, we adjust the bloom buffer size so that both dimensions
// have enough exact mip levels. This can slightly affect the aspect ratio causing
// some artifacts:
// - add some anamorphism (experimentally not visible)
// - visible bloom size changes with dynamic resolution in non-homogenous mode
// This allows us to use the 9 sample downsampling filter (instead of 13)
// for at least 4 levels.
uint32_t width = std::max(1u, uint32_t(std::floor(bloomWidth)));
uint32_t height = std::max(1u, uint32_t(std::floor(bloomHeight)));
width &= ~((1 << 4) - 1); // at least 4 levels
height &= ~((1 << 4) - 1);
bloomWidth = float(width);
bloomHeight = float(height);
}
bool threshold = inoutBloomOptions.threshold;
while (2 * bloomWidth < float(desc.width) || 2 * bloomHeight < float(desc.height)) {
if (inoutBloomOptions.quality == QualityLevel::LOW ||
inoutBloomOptions.quality == QualityLevel::MEDIUM) {
input = downscalePass(fg, input, {
.width = (desc.width = std::max(1u, desc.width / 2)),
.height = (desc.height = std::max(1u, desc.height / 2)),
.format = outFormat
},
threshold, inoutBloomOptions.highlight, threshold);
threshold = false; // we do the thresholding only once during down sampling
} else if (inoutBloomOptions.quality == QualityLevel::HIGH ||
inoutBloomOptions.quality == QualityLevel::ULTRA) {
// In high quality mode, we increase the size of the bloom buffer such that the
// first scaling is less than 2x, and we increase the number of levels accordingly.
if (bloomWidth * 2.0f > 2048.0f || bloomHeight * 2.0f > 2048.0f) {
// but we can't scale above the h/w guaranteed minspec
break;
}
bloomWidth *= 2.0f;
bloomHeight *= 2.0f;
inoutBloomOptions.levels++;
}
}
// convert back to integer width/height
uint32_t const width = std::max(1u, uint32_t(std::floor(bloomWidth)));
uint32_t const height = std::max(1u, uint32_t(std::floor(bloomHeight)));
input = downscalePass(fg, input,
{ .width = width, .height = height, .format = outFormat },
threshold, inoutBloomOptions.highlight, threshold);
struct BloomPassData {
FrameGraphId<FrameGraphTexture> in;
FrameGraphId<FrameGraphTexture> out;
FrameGraphId<FrameGraphTexture> stage;
uint32_t outRT[kMaxBloomLevels];
uint32_t stageRT[kMaxBloomLevels];
};
// downsample phase
// Creating a mip-chain poses a "feedback" loop problem on some GPU. We will disable
// Bloom on these.
// See: https://github.com/google/filament/issues/2338
auto& bloomDownsamplePass = fg.addPass<BloomPassData>("Bloom Downsample",
[&](FrameGraph::Builder& builder, auto& data) {
data.in = builder.sample(input);
data.out = builder.createTexture("Bloom Out Texture", {
.width = width,
.height = height,
@@ -1933,165 +2006,107 @@ PostProcessManager::BloomPassOutput PostProcessManager::bloomPass(FrameGraph& fg
});
data.out = builder.sample(data.out);
data.stage = builder.createTexture("Bloom Stage Texture", {
.width = width,
.height = height,
.levels = inoutBloomOptions.levels,
.format = outFormat
});
data.stage = builder.sample(data.stage);
for (size_t i = 0; i < inoutBloomOptions.levels; i++) {
auto out = builder.createSubresource(data.out, "Bloom Out Texture mip",
{ .level = uint8_t(i) });
auto stage = builder.createSubresource(data.stage,
"Bloom Stage Texture mip", { .level = uint8_t(i) });
if (i == 0) {
// this causes the last blit above to render into this mip
fg.forwardResource(out, input);
}
builder.declareRenderPass(out, &data.outRT[i]);
builder.declareRenderPass(stage, &data.stageRT[i]);
}
},
[=](FrameGraphResources const& resources,
auto const& data, DriverApi& driver) {
auto hwIn = resources.getTexture(data.in);
// TODO: if downsampling is not exactly a multiple of two, use the 13 samples
// filter. This is generally the accepted solution, however, the 13 samples
// filter is not correct either when we don't sample at integer coordinates,
// but it seems ot create less artifacts.
// A better solution might be to use the filter described in
// Castaño, 2013, "Shadow Mapping Summary Part 1", which is 5x5 filter with
// 9 samples, but works at all coordinates.
auto hwOut = resources.getTexture(data.out);
auto hwStage = resources.getTexture(data.stage);
auto const& material = getPostProcessMaterial("bloomDownsample");
auto const* ma = material.getMaterial(mEngine);
auto const& material9 = getPostProcessMaterial("bloomDownsample9");
auto const& material13 = getPostProcessMaterial("bloomDownsample");
FMaterialInstance* mis[] = {
ma->createInstance("bloomDownsample-ping"),
ma->createInstance("bloomDownsample-pong"),
ma->createInstance("bloomDownsample-first"),
};
auto* mi9 = material9.getMaterialInstance(mEngine);
auto* mi13 = material13.getMaterialInstance(mEngine);
mis[0]->setParameter("source", hwOut, {
mi9->setParameter("source", hwOut, {
.filterMag = SamplerMagFilter::LINEAR,
.filterMin = SamplerMinFilter::LINEAR_MIPMAP_NEAREST
});
.filterMin = SamplerMinFilter::LINEAR_MIPMAP_NEAREST });
mis[1]->setParameter("source", hwStage, {
mi13->setParameter("source", hwOut, {
.filterMag = SamplerMagFilter::LINEAR,
.filterMin = SamplerMinFilter::LINEAR_MIPMAP_NEAREST
});
mis[2]->setParameter("source", hwIn, {
.filterMag = SamplerMagFilter::LINEAR,
.filterMin = SamplerMinFilter::LINEAR_MIPMAP_NEAREST
});
.filterMin = SamplerMinFilter::LINEAR_MIPMAP_NEAREST });
for (auto* mi : mis) {
mi->setParameter("level", 0.0f);
mi->setParameter("threshold", inoutBloomOptions.threshold ? 1.0f : 0.0f);
mi->setParameter("invHighlight", std::isinf(inoutBloomOptions.highlight)
? 0.0f : 1.0f / inoutBloomOptions.highlight);
mi->commit(driver);
}
mi9->commit(driver);
mi13->commit(driver);
const PipelineState pipeline(material.getPipelineState(mEngine));
{ // first iteration
auto hwDstRT = resources.getRenderPassInfo(data.outRT[0]);
hwDstRT.params.flags.discardStart = TargetBufferFlags::COLOR;
hwDstRT.params.flags.discardEnd = TargetBufferFlags::NONE;
mis[2]->use(driver);
render(hwDstRT, pipeline, driver);
}
// PipelineState for both materials should be the same
const PipelineState pipeline(material9.getPipelineState(mEngine));
for (size_t i = 1; i < inoutBloomOptions.levels; i++) {
const size_t parity = 1u - (i & 0x1u);
auto hwDstRT = resources.getRenderPassInfo(parity ? data.outRT[i] : data.stageRT[i]);
auto hwDstRT = resources.getRenderPassInfo(data.outRT[i]);
hwDstRT.params.flags.discardStart = TargetBufferFlags::COLOR;
hwDstRT.params.flags.discardEnd = TargetBufferFlags::NONE;
mis[parity]->setParameter("level", float(i - 1));
mis[parity]->commit(driver);
mis[parity]->use(driver);
// if downsampling is a multiple of 2 in each dimension we can use the
// 9 samples filter.
auto vp = resources.getRenderPassInfo(data.outRT[i-1]).params.viewport;
auto* const mi = (vp.width & 1 || vp.height & 1) ? mi13 : mi9;
mi->use(driver);
driver.setMinMaxLevels(hwOut, i - 1, i - 1); // this offsets baseLevel to i-1
render(hwDstRT, pipeline, driver);
}
for (auto& mi : mis) {
mEngine.destroy(mi);
}
driver.setMinMaxLevels(hwOut, 0, inoutBloomOptions.levels - 1);
});
FrameGraphId<FrameGraphTexture> output = bloomDownsamplePass->out;
FrameGraphId<FrameGraphTexture> stage = bloomDownsamplePass->stage;
// output of bloom downsample pass becomes input of next (flare) pass
input = bloomDownsamplePass->out;
// flare pass
auto flare = flarePass(fg, bloomDownsamplePass->out, width, height, outFormat, inoutBloomOptions);
auto flare = flarePass(fg, input, width, height, outFormat, inoutBloomOptions);
// upsample phase
auto& bloomUpsamplePass = fg.addPass<BloomPassData>("Bloom Upsample",
[&](FrameGraph::Builder& builder, auto& data) {
data.out = builder.sample(output);
data.stage = builder.sample(stage);
data.out = builder.sample(input);
for (size_t i = 0; i < inoutBloomOptions.levels; i++) {
auto out = builder.createSubresource(data.out, "Bloom Out Texture mip",
{ .level = uint8_t(i) });
auto staging = builder.createSubresource(data.stage,
"Bloom Stage Texture mip", { .level = uint8_t(i) });
builder.declareRenderPass(out, &data.outRT[i]);
builder.declareRenderPass(staging, &data.stageRT[i]);
}
},
[=](FrameGraphResources const& resources, auto const& data, DriverApi& driver) {
auto hwOut = resources.getTexture(data.out);
auto hwStage = resources.getTexture(data.stage);
auto const& outDesc = resources.getDescriptor(data.out);
auto const& material = getPostProcessMaterial("bloomUpsample");
auto const* ma = material.getMaterial(mEngine);
FMaterialInstance* mis[] = {
ma->createInstance("bloomUpsample-ping"),
ma->createInstance("bloomUpsample-pong"),
};
mis[0]->setParameter("source", hwOut, {
auto* mi = material.getMaterialInstance(mEngine);
mi->setParameter("source", hwOut, {
.filterMag = SamplerMagFilter::LINEAR,
.filterMin = SamplerMinFilter::LINEAR_MIPMAP_NEAREST
});
mis[1]->setParameter("source", hwStage, {
.filterMag = SamplerMagFilter::LINEAR,
.filterMin = SamplerMinFilter::LINEAR_MIPMAP_NEAREST
});
.filterMin = SamplerMinFilter::LINEAR_MIPMAP_NEAREST});
mi->use(driver);
PipelineState pipeline(material.getPipelineState(mEngine));
pipeline.rasterState.blendFunctionSrcRGB = BlendFunction::ONE;
pipeline.rasterState.blendFunctionDstRGB = BlendFunction::ONE;
for (size_t j = inoutBloomOptions.levels, i = j - 1; i >= 1; i--, j++) {
const size_t parity = 1u - (j % 2u);
auto hwDstRT = resources.getRenderPassInfo(
parity ? data.outRT[i - 1] : data.stageRT[i - 1]);
auto hwDstRT = resources.getRenderPassInfo(data.outRT[i - 1]);
hwDstRT.params.flags.discardStart = TargetBufferFlags::NONE; // b/c 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);
mis[parity]->setParameter("resolution", float4{ w, h, 1.0f / w, 1.0f / h });
mis[parity]->setParameter("level", float(i));
mis[parity]->commit(driver);
mis[parity]->use(driver);
mi->setParameter("resolution", float4{ w, h, 1.0f / w, 1.0f / h });
mi->commit(driver);
driver.setMinMaxLevels(hwOut, i, i); // this offsets baseLevel to i
render(hwDstRT, pipeline, driver);
}
for (auto& mi : mis) {
mEngine.destroy(mi);
}
// Every other level is missing from the out texture, so we need to do
// blits to complete the chain.
const SamplerMagFilter filter = SamplerMagFilter::NEAREST;
for (size_t i = 1; i < inoutBloomOptions.levels; i += 2) {
auto in = resources.getRenderPassInfo(data.stageRT[i]);
auto out = resources.getRenderPassInfo(data.outRT[i]);
driver.blit(TargetBufferFlags::COLOR, out.target, out.params.viewport,
in.target, in.params.viewport, filter);
}
driver.setMinMaxLevels(hwOut, 0, inoutBloomOptions.levels - 1);
});
return { bloomUpsamplePass->out, flare };
@@ -3036,7 +3051,6 @@ FrameGraphId<FrameGraphTexture> PostProcessManager::vsmMipmapPass(FrameGraph& fg
.filterMag = SamplerMagFilter::LINEAR,
.filterMin = SamplerMinFilter::LINEAR_MIPMAP_NEAREST
});
mi->setParameter("level", uint32_t(level));
mi->setParameter("layer", uint32_t(layer));
mi->setParameter("uvscale", 1.0f / float(dim));
mi->commit(driver);

View File

@@ -292,6 +292,11 @@ private:
FrameGraphId<FrameGraphTexture> input, backend::TextureFormat outFormat,
BloomOptions& inoutBloomOptions, math::float2 scale) noexcept;
FrameGraphId<FrameGraphTexture> downscalePass(FrameGraph& fg,
FrameGraphId<FrameGraphTexture> input,
FrameGraphTexture::Descriptor const& outDesc,
bool threshold, float highlight, bool fireflies) noexcept;
void commitAndRender(FrameGraphResources::RenderPassInfo const& out,
PostProcessMaterial const& material, uint8_t variant,
backend::DriverApi& driver) const noexcept;

View File

@@ -1080,8 +1080,9 @@ void FView::setSoftShadowOptions(SoftShadowOptions options) noexcept {
void FView::setBloomOptions(BloomOptions options) noexcept {
options.dirtStrength = math::saturate(options.dirtStrength);
options.levels = math::clamp(options.levels, uint8_t(1), uint8_t(11));
options.resolution = math::clamp(options.resolution, 1u << options.levels, 2048u);
options.resolution = math::clamp(options.resolution, 2u, 2048u);
options.levels = math::clamp(options.levels, uint8_t(1),
FTexture::maxLevelCount(options.resolution));
options.anamorphism = math::clamp(options.anamorphism, 1.0f/32.0f, 32.0f);
options.highlight = std::max(10.0f, options.highlight);
mBloomOptions = options;

View File

@@ -5,18 +5,6 @@ material {
type : sampler2d,
name : source,
precision: medium
},
{
type : float,
name : level
},
{
type : float,
name : threshold
},
{
type : float,
name : invHighlight
}
],
variables : [
@@ -37,85 +25,43 @@ fragment {
void dummy(){}
void threshold(inout vec3 c) {
// threshold everything below 1.0
c = max(vec3(0.0), c - 1.0);
// crush everything above 1
highp float f = max3(c);
c *= 1.0 / (1.0 + f * materialParams.invHighlight);
}
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;
// see SIGGRAPH 2014: Advances in Real-Time Rendering
// "Next Generation Post-Processing in Call of Duty Advanced Warfare"
// Jorge Jimenez
vec3 c = textureLod(materialParams_source, uv, lod).rgb;
vec3 c = textureLod(materialParams_source, uv, 0.0).rgb;
// The offsets below are in "source" texture space
vec3 lt = textureLodOffset(materialParams_source, uv, lod, ivec2(-1, -1)).rgb;
vec3 rt = textureLodOffset(materialParams_source, uv, lod, ivec2( 1, -1)).rgb;
vec3 rb = textureLodOffset(materialParams_source, uv, lod, ivec2( 1, 1)).rgb;
vec3 lb = textureLodOffset(materialParams_source, uv, lod, ivec2(-1, 1)).rgb;
vec3 lt = textureLodOffset(materialParams_source, uv, 0.0, ivec2(-1, -1)).rgb;
vec3 rt = textureLodOffset(materialParams_source, uv, 0.0, ivec2( 1, -1)).rgb;
vec3 rb = textureLodOffset(materialParams_source, uv, 0.0, ivec2( 1, 1)).rgb;
vec3 lb = textureLodOffset(materialParams_source, uv, 0.0, ivec2(-1, 1)).rgb;
vec3 lt2 = textureLodOffset(materialParams_source, uv, lod, ivec2(-2, -2)).rgb;
vec3 rt2 = textureLodOffset(materialParams_source, uv, lod, ivec2( 2, -2)).rgb;
vec3 rb2 = textureLodOffset(materialParams_source, uv, lod, ivec2( 2, 2)).rgb;
vec3 lb2 = textureLodOffset(materialParams_source, uv, lod, ivec2(-2, 2)).rgb;
vec3 lt2 = textureLodOffset(materialParams_source, uv, 0.0, ivec2(-2, -2)).rgb;
vec3 rt2 = textureLodOffset(materialParams_source, uv, 0.0, ivec2( 2, -2)).rgb;
vec3 rb2 = textureLodOffset(materialParams_source, uv, 0.0, ivec2( 2, 2)).rgb;
vec3 lb2 = textureLodOffset(materialParams_source, uv, 0.0, ivec2(-2, 2)).rgb;
vec3 l = textureLodOffset(materialParams_source, uv, lod, ivec2(-2, 0)).rgb;
vec3 t = textureLodOffset(materialParams_source, uv, lod, ivec2( 0, -2)).rgb;
vec3 r = textureLodOffset(materialParams_source, uv, lod, ivec2( 2, 0)).rgb;
vec3 b = textureLodOffset(materialParams_source, uv, lod, ivec2( 0, 2)).rgb;
vec3 l = textureLodOffset(materialParams_source, uv, 0.0, ivec2(-2, 0)).rgb;
vec3 t = textureLodOffset(materialParams_source, uv, 0.0, ivec2( 0, -2)).rgb;
vec3 r = textureLodOffset(materialParams_source, uv, 0.0, ivec2( 2, 0)).rgb;
vec3 b = textureLodOffset(materialParams_source, uv, 0.0, ivec2( 0, 2)).rgb;
// five h4x4 boxes
vec3 c0, c1;
if (materialParams.level <= 0.5) {
if (materialParams.threshold > 0.0) {
// Threshold the first level blur
threshold(c);
threshold(lt);
threshold(rt);
threshold(rb);
threshold(lb);
threshold(lt2);
threshold(rt2);
threshold(rb2);
threshold(lb2);
threshold(l);
threshold(t);
threshold(r);
threshold(b);
}
// Also apply fireflies (flickering) filtering
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);
} else {
// 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);
}
// 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);
// weighted average of the five boxes
postProcess.color.rgb = c0 * 0.5 + c1 * 0.125;

View File

@@ -0,0 +1,98 @@
material {
name : bloomDownsample2x,
parameters : [
{
type : sampler2d,
name : source,
precision: medium
},
{
type : float,
name : level
},
{
type : float,
name : threshold
},
{
type : float,
name : fireflies
},
{
type : float,
name : invHighlight
}
],
variables : [
vertex
],
domain : postprocess,
depthWrite : false,
depthCulling : false
}
vertex {
void postProcessVertex(inout PostProcessVertexInputs postProcess) {
postProcess.vertex.xy = uvToRenderTargetUV(postProcess.normalizedUV);
}
}
fragment {
void dummy(){}
void threshold(inout vec3 c) {
// threshold everything below 1.0
c = max(vec3(0.0), c - 1.0);
// crush everything above 1
highp float f = max3(c);
c *= 1.0 / (1.0 + f * materialParams.invHighlight);
}
void postProcess(inout PostProcessInputs postProcess) {
float lod = materialParams.level;
highp vec2 size = vec2(textureSize(materialParams_source, int(lod)));
highp vec2 texelSize = vec2(1.0) / size;
// Castaño, 2013, "Shadow Mapping Summary Part 1"
// 3x3 gaussian filter with 4 linear samples
vec2 offset = vec2(0.5);
highp vec2 uv = (variable_vertex.xy * size) + offset;
highp vec2 base = (floor(uv) - offset) * texelSize;
highp vec2 st = fract(uv);
vec2 uw = vec2(3.0 - 2.0 * st.x, 1.0 + 2.0 * st.x);
vec2 vw = vec2(3.0 - 2.0 * st.y, 1.0 + 2.0 * st.y);
highp vec2 u = vec2((2.0 - st.x) / uw.x - 1.0, st.x / uw.y + 1.0) * texelSize.x;
highp vec2 v = vec2((2.0 - st.y) / vw.x - 1.0, st.y / vw.y + 1.0) * texelSize.y;
vec3 c0 = textureLod(materialParams_source, base + vec2(u.x, v.x), lod).rgb;
vec3 c1 = textureLod(materialParams_source, base + vec2(u.y, v.x), lod).rgb;
vec3 c2 = textureLod(materialParams_source, base + vec2(u.x, v.y), lod).rgb;
vec3 c3 = textureLod(materialParams_source, base + vec2(u.y, v.y), lod).rgb;
float w0 = uw.x * vw.x * (1.0 / 16.0);
float w1 = uw.y * vw.x * (1.0 / 16.0);
float w2 = uw.x * vw.y * (1.0 / 16.0);
float w3 = uw.y * vw.y * (1.0 / 16.0);
if (materialParams.fireflies > 0.0) {
w0 /= (1.0 + max3(c0));
w1 /= (1.0 + max3(c1));
w2 /= (1.0 + max3(c2));
w3 /= (1.0 + max3(c3));
float w = 1.0 / (w0 + w1 + w2 + w3);
w0 *= w;
w1 *= w;
w2 *= w;
w3 *= w;
}
vec3 c = c0 * w0 + c1 * w1 + c2 * w2 + c3 * w3;
if (materialParams.threshold > 0.0) {
threshold(c);
}
postProcess.color.rgb = c;
}
}

View File

@@ -0,0 +1,65 @@
material {
name : bloomDownsample9,
parameters : [
{
type : sampler2d,
name : source,
precision: medium
}
],
variables : [
vertex
],
domain : postprocess,
depthWrite : false,
depthCulling : false
}
vertex {
void postProcessVertex(inout PostProcessVertexInputs postProcess) {
postProcess.vertex.xy = uvToRenderTargetUV(postProcess.normalizedUV);
}
}
fragment {
void dummy(){}
// see https://www.shadertoy.com/view/cslczj
// 6x6 downsampling kernel implemented via 9 bilinear samples
void postProcess(inout PostProcessInputs postProcess) {
highp vec2 uv = variable_vertex.xy;
highp vec2 size = vec2(1.0) / vec2(textureSize(materialParams_source, 0));
float o = 1.5 + 0.261629;
float wa = 7.46602 / 32.0;
float wb = 1.0 - wa * 2.0;
float wab = wa * wb;
float waa = wa * wa;
float wbb = wb * wb;
size *= o;
vec3 c = textureLod(materialParams_source, uv + vec2(0.0) , 0.0).rgb;
vec3 l = textureLod(materialParams_source, uv + vec2(-size.x, 0.0), 0.0).rgb;
vec3 r = textureLod(materialParams_source, uv + vec2( size.x, 0.0), 0.0).rgb;
vec3 b = textureLod(materialParams_source, uv + vec2( 0.0,-size.y), 0.0).rgb;
vec3 t = textureLod(materialParams_source, uv + vec2( 0.0, size.y), 0.0).rgb;
vec3 lb = textureLod(materialParams_source, uv + vec2(-size.x,-size.y), 0.0).rgb;
vec3 rb = textureLod(materialParams_source, uv + vec2( size.x,-size.y), 0.0).rgb;
vec3 lt = textureLod(materialParams_source, uv + vec2(-size.x, size.y), 0.0).rgb;
vec3 rt = textureLod(materialParams_source, uv + vec2( size.x, size.y), 0.0).rgb;
postProcess.color.rgb =
(c * wbb +
(l * wab +
(r * wab +
(b * wab +
(t * wab +
(lb * waa +
(rb * waa +
(lt * waa +
(rt * waa)))))))));
}
}

View File

@@ -10,10 +10,6 @@ material {
type : float4,
name : resolution,
precision: high
},
{
type : float,
name : level
}
],
variables : [
@@ -34,16 +30,15 @@ fragment {
void dummy(){}
void postProcess(inout PostProcessInputs postProcess) {
float lod = materialParams.level;
highp vec2 uv = variable_vertex.xy;
#if FILAMENT_QUALITY < FILAMENT_QUALITY_HIGH
highp vec4 d = vec4(materialParams.resolution.zw, -materialParams.resolution.zw) * 0.5;
vec3 c;
c = textureLod(materialParams_source, uv + d.zw, lod).rgb;
c += textureLod(materialParams_source, uv + d.xw, lod).rgb;
c += textureLod(materialParams_source, uv + d.xy, lod).rgb;
c += textureLod(materialParams_source, uv + d.zy, lod).rgb;
c = textureLod(materialParams_source, uv + d.zw, 0.0).rgb;
c += textureLod(materialParams_source, uv + d.xw, 0.0).rgb;
c += textureLod(materialParams_source, uv + d.xy, 0.0).rgb;
c += textureLod(materialParams_source, uv + d.zy, 0.0).rgb;
postProcess.color.rgb = c * 0.25;
#else
// see SIGGRAPH 2014: Advances in Real-Time Rendering
@@ -52,15 +47,15 @@ fragment {
const float radius = 1.0;
highp vec4 d = vec4(materialParams.resolution.zw, -materialParams.resolution.zw) * radius;
vec3 c0, c1;
c0 = textureLod(materialParams_source, uv + d.zw, lod).rgb;
c0 += textureLod(materialParams_source, uv + d.xw, lod).rgb;
c0 += textureLod(materialParams_source, uv + d.xy, lod).rgb;
c0 += textureLod(materialParams_source, uv + d.zy, lod).rgb;
c0 += 4.0 * textureLod(materialParams_source, uv, lod).rgb;
c1 = textureLod(materialParams_source, uv + vec2(d.z, 0.0), lod).rgb;
c1 += textureLod(materialParams_source, uv + vec2(0.0, d.w), lod).rgb;
c1 += textureLod(materialParams_source, uv + vec2(d.x, 0.0), lod).rgb;
c1 += textureLod(materialParams_source, uv + vec2( 0.0, d.y), lod).rgb;
c0 = textureLod(materialParams_source, uv + d.zw, 0.0).rgb;
c0 += textureLod(materialParams_source, uv + d.xw, 0.0).rgb;
c0 += textureLod(materialParams_source, uv + d.xy, 0.0).rgb;
c0 += textureLod(materialParams_source, uv + d.zy, 0.0).rgb;
c0 += 4.0 * textureLod(materialParams_source, uv, 0.0).rgb;
c1 = textureLod(materialParams_source, uv + vec2(d.z, 0.0), 0.0).rgb;
c1 += textureLod(materialParams_source, uv + vec2(0.0, d.w), 0.0).rgb;
c1 += textureLod(materialParams_source, uv + vec2(d.x, 0.0), 0.0).rgb;
c1 += textureLod(materialParams_source, uv + vec2( 0.0, d.y), 0.0).rgb;
postProcess.color.rgb = (c0 + 2.0 * c1) * (1.0 / 16.0);
#endif
}

View File

@@ -11,10 +11,6 @@ material {
name : coc,
precision: medium
},
{
type : int,
name : mip
},
{
type : float,
name : weightScale
@@ -65,19 +61,18 @@ void postProcess(inout PostProcessInputs postProcess) {
// the bilateral weights need to be scaled by to match the lower resolution
float weightScale = materialParams.weightScale;
float mip = float(materialParams.mip);
vec4 s01 = textureLodOffset(materialParams_color, uv, mip, ivec2(0, 1));
vec4 s11 = textureLodOffset(materialParams_color, uv, mip, ivec2(1, 1));
vec4 s10 = textureLodOffset(materialParams_color, uv, mip, ivec2(1, 0));
vec4 s00 = textureLodOffset(materialParams_color, uv, mip, ivec2(0, 0));
vec4 s01 = textureLodOffset(materialParams_color, uv, 0.0, ivec2(0, 1));
vec4 s11 = textureLodOffset(materialParams_color, uv, 0.0, ivec2(1, 1));
vec4 s10 = textureLodOffset(materialParams_color, uv, 0.0, ivec2(1, 0));
vec4 s00 = textureLodOffset(materialParams_color, uv, 0.0, ivec2(0, 0));
// fetch the 4 corresponding CoC (textureGather with LOD doesn't exist)
vec4 c;
c[0] = textureLodOffset(materialParams_coc, uv, mip, ivec2(0, 1)).r;
c[1] = textureLodOffset(materialParams_coc, uv, mip, ivec2(1, 1)).r;
c[2] = textureLodOffset(materialParams_coc, uv, mip, ivec2(1, 0)).r;
c[3] = textureLodOffset(materialParams_coc, uv, mip, ivec2(0, 0)).r;
c[0] = textureLodOffset(materialParams_coc, uv, 0.0, ivec2(0, 1)).r;
c[1] = textureLodOffset(materialParams_coc, uv, 0.0, ivec2(1, 1)).r;
c[2] = textureLodOffset(materialParams_coc, uv, 0.0, ivec2(1, 0)).r;
c[3] = textureLodOffset(materialParams_coc, uv, 0.0, ivec2(0, 0)).r;
float outCoc = downsampleCoC(c);
vec4 w = downsampleCocWeights(c, outCoc, weightScale);

View File

@@ -5,10 +5,6 @@ material {
type : sampler2d,
name : depth,
precision: high
},
{
type : int,
name : level
}
],
variables : [
@@ -30,9 +26,8 @@ fragment {
// We use a rotated grid sub-sample as it's cheap and gives good results
// See Scalable Ambient Obscurance by McGuire and al.
void postProcess(inout PostProcessInputs postProcess) {
int level = materialParams.level;
ivec2 icoord = ivec2(gl_FragCoord.xy);
postProcess.depth = texelFetch(materialParams_depth,
2 * icoord + ivec2(icoord.y & 1, icoord.x & 1), level).r;
2 * icoord + ivec2(icoord.y & 1, icoord.x & 1), 0).r;
}
}

View File

@@ -10,10 +10,6 @@ material {
type : int,
name : layer
},
{
type : int,
name : level
},
{
type : float,
name : uvscale
@@ -36,7 +32,6 @@ fragment {
void postProcess(inout PostProcessInputs postProcess) {
highp vec2 uv = gl_FragCoord.xy * materialParams.uvscale;
postProcess.color = textureLod(materialParams_color,
vec3(uv, materialParams.layer),
float(materialParams.level));
vec3(uv, materialParams.layer), 0.0);
}
}

View File

@@ -249,6 +249,8 @@ int parse(jsmntok_t const* tokens, int i, const char* jsonChunk, BloomOptions* o
i = parse(tokens, i + 1, jsonChunk, &out->enabled);
} else if (compare(tok, jsonChunk, "highlight") == 0) {
i = parse(tokens, i + 1, jsonChunk, &out->highlight);
} else if (compare(tok, jsonChunk, "quality") == 0) {
i = parse(tokens, i + 1, jsonChunk, &out->quality);
} else if (compare(tok, jsonChunk, "lensFlare") == 0) {
i = parse(tokens, i + 1, jsonChunk, &out->lensFlare);
} else if (compare(tok, jsonChunk, "starburst") == 0) {
@@ -291,6 +293,7 @@ std::ostream& operator<<(std::ostream& out, const BloomOptions& in) {
<< "\"threshold\": " << to_string(in.threshold) << ",\n"
<< "\"enabled\": " << to_string(in.enabled) << ",\n"
<< "\"highlight\": " << (in.highlight) << ",\n"
<< "\"quality\": " << (in.quality) << ",\n"
<< "\"lensFlare\": " << to_string(in.lensFlare) << ",\n"
<< "\"starburst\": " << to_string(in.starburst) << ",\n"
<< "\"chromaticAberration\": " << (in.chromaticAberration) << ",\n"

View File

@@ -777,6 +777,10 @@ void ViewerGui::updateUserInterface() {
ImGui::SliderInt("Levels", &levels, 3, 11);
mSettings.view.bloom.levels = levels;
int quality = (int) mSettings.view.bloom.quality;
ImGui::SliderInt("Bloom Quality", &quality, 0, 3);
mSettings.view.bloom.quality = (View::QualityLevel) quality;
ImGui::Checkbox("Lens Flare", &mSettings.view.bloom.lensFlare);
}

View File

@@ -26,6 +26,7 @@ Filament.loadGeneratedExtensions = function() {
threshold: true,
enabled: false,
highlight: 1000.0,
quality: Filament.View$QualityLevel.LOW,
lensFlare: false,
starburst: true,
chromaticAberration: 0.005,

View File

@@ -1253,6 +1253,16 @@ export interface View$BloomOptions {
* limit highlights to this value before bloom [10, +inf]
*/
highlight?: number;
/**
* Bloom quality level.
* LOW (default): use a more optimized down-sampling filter, however there can be artifacts
* with dynamic resolution, this can be alleviated by using the homogenous mode.
* MEDIUM: Good balance between quality and performance.
* HIGH: In this mode the bloom resolution is automatically increased to avoid artifacts.
* This mode can be significantly slower on mobile, especially at high resolution.
* This mode greatly improves the anamorphic bloom.
*/
quality?: View$QualityLevel;
/**
* enable screen-space lens flare
*/

View File

@@ -30,6 +30,7 @@ value_object<View::BloomOptions>("View$BloomOptions")
.field("threshold", &View::BloomOptions::threshold)
.field("enabled", &View::BloomOptions::enabled)
.field("highlight", &View::BloomOptions::highlight)
.field("quality", &View::BloomOptions::quality)
.field("lensFlare", &View::BloomOptions::lensFlare)
.field("starburst", &View::BloomOptions::starburst)
.field("chromaticAberration", &View::BloomOptions::chromaticAberration)