Files
filament/filament/src/PostProcessManager.cpp
T
Mathias Agopian 0fb04c576d improve structure pass performance when not used
Move the command generation of the structure pass into its execute 
block, meaning that if the structure pass is culled, the command 
generation is now skipped too.
2022-01-18 12:21:36 -08:00

2681 lines
123 KiB
C++

/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifdef FILAMENT_TARGET_MOBILE
# define DOF_DEFAULT_RING_COUNT 3
# define DOF_DEFAULT_MAX_COC 24
#else
# define DOF_DEFAULT_RING_COUNT 5
# define DOF_DEFAULT_MAX_COC 32
#endif
#include "PostProcessManager.h"
#include "details/Engine.h"
#include "fg2/FrameGraph.h"
#include "fg2/FrameGraphResources.h"
#include "fsr.h"
#include "RenderPass.h"
#include "details/Camera.h"
#include "details/ColorGrading.h"
#include "details/Material.h"
#include "details/MaterialInstance.h"
#include "details/Texture.h"
#include "generated/resources/materials.h"
#include <filament/MaterialEnums.h>
#include <math/half.h>
#include <math/mat2.h>
#include <algorithm>
#include <limits>
namespace filament {
using namespace utils;
using namespace math;
using namespace backend;
static constexpr uint8_t kMaxBloomLevels = 12u;
static_assert(kMaxBloomLevels >= 3, "We require at least 3 bloom levels");
constexpr static float halton(unsigned int i, unsigned int b) noexcept {
// skipping a bunch of entries makes the average of the sequence closer to 0.5
i += 409;
float f = 1.0f;
float r = 0.0f;
while (i > 0u) {
f /= float(b);
r += f * float(i % b);
i /= b;
}
return r;
}
// ------------------------------------------------------------------------------------------------
PostProcessManager::PostProcessMaterial::PostProcessMaterial() noexcept {
mEngine = nullptr;
mData = nullptr;
mMaterial = nullptr; // aliased to mEngine
}
PostProcessManager::PostProcessMaterial::PostProcessMaterial(FEngine& engine,
uint8_t const* data, int size) noexcept
: PostProcessMaterial() {
mEngine = &engine;
mData = data;
mSize = size;
}
PostProcessManager::PostProcessMaterial::PostProcessMaterial(
PostProcessManager::PostProcessMaterial&& rhs) noexcept
: PostProcessMaterial() {
using namespace std;
swap(mEngine, rhs.mEngine);
swap(mData, rhs.mData);
swap(mSize, rhs.mSize);
swap(mHasMaterial, rhs.mHasMaterial);
}
PostProcessManager::PostProcessMaterial& PostProcessManager::PostProcessMaterial::operator=(
PostProcessManager::PostProcessMaterial&& rhs) noexcept {
using namespace std;
swap(mEngine, rhs.mEngine);
swap(mData, rhs.mData);
swap(mSize, rhs.mSize);
return *this;
}
PostProcessManager::PostProcessMaterial::~PostProcessMaterial() {
assert_invariant(!mHasMaterial || mMaterial == nullptr);
}
void PostProcessManager::PostProcessMaterial::terminate(FEngine& engine) noexcept {
if (mHasMaterial) {
engine.destroy(mMaterial);
// this is only needed for validation in the dtor in debug builds
#ifndef NDEBUG
mMaterial = nullptr;
mHasMaterial = false;
} else {
mEngine = nullptr;
mData = nullptr;
#endif
}
}
UTILS_NOINLINE
FMaterial* PostProcessManager::PostProcessMaterial::loadMaterial() const noexcept {
// TODO: After all materials using this class have been converted to the post-process material
// domain, load both OPAQUE and TRANSPARENT variants here.
mHasMaterial = true;
mMaterial = upcast(Material::Builder().package(mData, mSize).build(*mEngine));
return mMaterial;
}
FMaterial* PostProcessManager::PostProcessMaterial::assertMaterial() const noexcept {
if (UTILS_UNLIKELY(!mHasMaterial)) {
return loadMaterial();
}
return mMaterial;
}
PipelineState PostProcessManager::PostProcessMaterial::getPipelineState(uint8_t variant) const noexcept {
FMaterial* const material = assertMaterial();
return {
.program = material->getProgram(variant),
.rasterState = material->getRasterState(),
.scissor = material->getDefaultInstance()->getScissor()
};
}
FMaterial* PostProcessManager::PostProcessMaterial::getMaterial() const {
assertMaterial();
return mMaterial;
}
FMaterialInstance* PostProcessManager::PostProcessMaterial::getMaterialInstance() const {
FMaterial* const material = assertMaterial();
return material->getDefaultInstance();
}
// ------------------------------------------------------------------------------------------------
PostProcessManager::PostProcessManager(FEngine& engine) noexcept
: mEngine(engine),
mHaltonSamples{
{ filament::halton( 0, 2), filament::halton( 0, 3) },
{ filament::halton( 1, 2), filament::halton( 1, 3) },
{ filament::halton( 2, 2), filament::halton( 2, 3) },
{ filament::halton( 3, 2), filament::halton( 3, 3) },
{ filament::halton( 4, 2), filament::halton( 4, 3) },
{ filament::halton( 5, 2), filament::halton( 5, 3) },
{ filament::halton( 6, 2), filament::halton( 6, 3) },
{ filament::halton( 7, 2), filament::halton( 7, 3) },
{ filament::halton( 8, 2), filament::halton( 8, 3) },
{ filament::halton( 9, 2), filament::halton( 9, 3) },
{ filament::halton(10, 2), filament::halton(10, 3) },
{ filament::halton(11, 2), filament::halton(11, 3) },
{ filament::halton(12, 2), filament::halton(12, 3) },
{ filament::halton(13, 2), filament::halton(13, 3) },
{ filament::halton(14, 2), filament::halton(14, 3) },
{ filament::halton(15, 2), filament::halton(15, 3) }},
mWorkaroundSplitEasu(false)
{
}
UTILS_NOINLINE
void PostProcessManager::registerPostProcessMaterial(utils::StaticString name, uint8_t const* data, int size) {
mMaterialRegistry.try_emplace(name, mEngine, data, size);
}
PostProcessManager::PostProcessMaterial& PostProcessManager::getPostProcessMaterial(utils::StaticString name) noexcept {
assert_invariant(mMaterialRegistry.find(name) != mMaterialRegistry.end());
return mMaterialRegistry[name];
}
#define MATERIAL(n) MATERIALS_ ## n ## _DATA, MATERIALS_ ## n ## _SIZE
struct MaterialInfo {
utils::StaticString name;
uint8_t const* data;
int size;
};
static const MaterialInfo sMaterialList[] = {
{ "bilateralBlur", MATERIAL(BILATERALBLUR) },
{ "bilateralBlurBentNormals", MATERIAL(BILATERALBLURBENTNORMALS) },
{ "blitLow", MATERIAL(BLITLOW) },
{ "bloomDownsample", MATERIAL(BLOOMDOWNSAMPLE) },
{ "bloomUpsample", MATERIAL(BLOOMUPSAMPLE) },
{ "colorGrading", MATERIAL(COLORGRADING) },
{ "colorGradingAsSubpass", MATERIAL(COLORGRADINGASSUBPASS) },
{ "customResolveAsSubpass", MATERIAL(CUSTOMRESOLVEASSUBPASS) },
{ "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) },
{ "saoBentNormals", MATERIAL(SAOBENTNORMALS) },
{ "separableGaussianBlur1", MATERIAL(SEPARABLEGAUSSIANBLUR1) },
{ "separableGaussianBlur2", MATERIAL(SEPARABLEGAUSSIANBLUR2) },
{ "separableGaussianBlur3", MATERIAL(SEPARABLEGAUSSIANBLUR3) },
{ "separableGaussianBlur4", MATERIAL(SEPARABLEGAUSSIANBLUR4) },
{ "taa", MATERIAL(TAA) },
{ "vsmMipmap", MATERIAL(VSMMIPMAP) },
{ "fsr_easu", MATERIAL(FSR_EASU) },
{ "fsr_easu_mobile", MATERIAL(FSR_EASU_MOBILE) },
{ "fsr_easu_mobileF", MATERIAL(FSR_EASU_MOBILEF) },
{ "fsr_rcas", MATERIAL(FSR_RCAS) },
};
void PostProcessManager::init() noexcept {
auto& engine = mEngine;
DriverApi& driver = engine.getDriverApi();
//FDebugRegistry& debugRegistry = engine.getDebugRegistry();
//debugRegistry.registerProperty("d.ssao.sampleCount", &engine.debug.ssao.sampleCount);
//debugRegistry.registerProperty("d.ssao.spiralTurns", &engine.debug.ssao.spiralTurns);
//debugRegistry.registerProperty("d.ssao.kernelSize", &engine.debug.ssao.kernelSize);
//debugRegistry.registerProperty("d.ssao.stddev", &engine.debug.ssao.stddev);
mWorkaroundSplitEasu = driver.isWorkaroundNeeded(Workaround::SPLIT_EASU);
#pragma nounroll
for (auto const& info : sMaterialList) {
registerPostProcessMaterial(info.name, info.data, info.size);
}
mDummyOneTexture = driver.createTexture(SamplerType::SAMPLER_2D, 1,
TextureFormat::RGBA8, 1, 1, 1, 1, TextureUsage::DEFAULT);
mDummyOneTextureArray = driver.createTexture(SamplerType::SAMPLER_2D_ARRAY, 1,
TextureFormat::RGBA8, 1, 1, 1, 1, TextureUsage::DEFAULT);
mDummyZeroTexture = driver.createTexture(SamplerType::SAMPLER_2D, 1,
TextureFormat::RGBA8, 1, 1, 1, 1, TextureUsage::DEFAULT);
mStarburstTexture = driver.createTexture(SamplerType::SAMPLER_2D, 1,
TextureFormat::R8, 1, 256, 1, 1, TextureUsage::DEFAULT);
PixelBufferDescriptor dataOne(driver.allocate(4), 4, PixelDataFormat::RGBA, PixelDataType::UBYTE);
PixelBufferDescriptor dataOneArray(driver.allocate(4), 4, PixelDataFormat::RGBA, PixelDataType::UBYTE);
PixelBufferDescriptor dataZero(driver.allocate(4), 4, PixelDataFormat::RGBA, PixelDataType::UBYTE);
PixelBufferDescriptor dataStarburst(driver.allocate(256), 256, PixelDataFormat::R, PixelDataType::UBYTE);
*static_cast<uint32_t *>(dataOne.buffer) = 0xFFFFFFFF;
*static_cast<uint32_t *>(dataOneArray.buffer) = 0xFFFFFFFF;
*static_cast<uint32_t *>(dataZero.buffer) = 0;
std::generate_n((uint8_t*)dataStarburst.buffer, 256,
[&dist = mUniformDistribution, &gen = mEngine.getRandomEngine()]() {
float r = 0.5f + 0.5f * dist(gen);
return uint8_t(r * 255.0f);
});
driver.update2DImage(mDummyOneTexture, 0, 0, 0, 1, 1, std::move(dataOne));
driver.update3DImage(mDummyOneTextureArray, 0, 0, 0, 0, 1, 1, 1, std::move(dataOneArray));
driver.update2DImage(mDummyZeroTexture, 0, 0, 0, 1, 1, std::move(dataZero));
driver.update2DImage(mStarburstTexture, 0, 0, 0, 256, 1, std::move(dataStarburst));
}
void PostProcessManager::terminate(DriverApi& driver) noexcept {
FEngine& engine = mEngine;
driver.destroyTexture(mDummyOneTexture);
driver.destroyTexture(mDummyOneTextureArray);
driver.destroyTexture(mDummyZeroTexture);
driver.destroyTexture(mStarburstTexture);
auto first = mMaterialRegistry.begin();
auto last = mMaterialRegistry.end();
while (first != last) {
first.value().terminate(engine);
++first;
}
}
UTILS_NOINLINE
void PostProcessManager::commitAndRender(FrameGraphResources::RenderPassInfo const& out,
PostProcessMaterial const& material, uint8_t variant, DriverApi& driver) const noexcept {
FMaterialInstance* const mi = material.getMaterialInstance();
mi->commit(driver);
mi->use(driver);
driver.beginRenderPass(out.target, out.params);
driver.draw(material.getPipelineState(variant), mEngine.getFullScreenRenderPrimitive());
driver.endRenderPass();
}
UTILS_ALWAYS_INLINE
void PostProcessManager::commitAndRender(FrameGraphResources::RenderPassInfo const& out,
PostProcessMaterial const& material, DriverApi& driver) const noexcept {
commitAndRender(out, material, 0, driver);
}
// ------------------------------------------------------------------------------------------------
FrameGraphId<FrameGraphTexture> PostProcessManager::structure(FrameGraph& fg,
RenderPass const& pass, uint8_t structureRenderFlags,
uint32_t width, uint32_t height,
StructurePassConfig const& config) noexcept {
const float scale = config.scale;
// structure pass -- automatically culled if not used, currently used by:
// - ssao
// - contact shadows
// It consists of a mipmapped depth pass, tuned for SSAO
struct StructurePassData {
FrameGraphId<FrameGraphTexture> depth;
FrameGraphId<FrameGraphTexture> picking;
};
// sanitize a bit the user provided scaling factor
width = std::max(32u, (uint32_t)std::ceil(width * scale));
height = std::max(32u, (uint32_t)std::ceil(height * scale));
// We limit the lowest lod size to 32 pixels (which is where the -5 comes from)
const size_t levelCount = std::min(8, FTexture::maxLevelCount(width, height) - 5);
assert_invariant(levelCount >= 1);
// generate depth pass at the requested resolution
auto& structurePass = fg.addPass<StructurePassData>("Structure Pass",
[&](FrameGraph::Builder& builder, auto& data) {
data.depth = builder.createTexture("Structure Buffer", {
.width = width, .height = height,
.levels = uint8_t(levelCount),
.format = TextureFormat::DEPTH32F });
// workaround: since we have levels, this implies SAMPLEABLE (because of the gl
// backend, which implements non-sampleables with renderbuffers, which don't have levels).
// (should the gl driver revert to textures, in that case?)
data.depth = builder.write(data.depth,
FrameGraphTexture::Usage::DEPTH_ATTACHMENT | FrameGraphTexture::Usage::SAMPLEABLE);
if (config.picking) {
data.picking = builder.createTexture("Picking Buffer", {
.width = width, .height = height,
.format = TextureFormat::RG32UI });
data.picking = builder.write(data.picking,
FrameGraphTexture::Usage::COLOR_ATTACHMENT);
}
builder.declareRenderPass("Structure Target", {
.attachments = { .color = { data.picking }, .depth = data.depth },
.clearFlags = TargetBufferFlags::COLOR0 | TargetBufferFlags::DEPTH
});
},
[=, renderPass = pass](FrameGraphResources const& resources,
auto const& data, DriverApi& driver) mutable {
auto out = resources.getRenderPassInfo();
renderPass.setRenderFlags(structureRenderFlags);
renderPass.appendCommands(RenderPass::CommandTypeFlags::SSAO);
renderPass.sortCommands();
renderPass.execute(resources.getPassName(), out.target, out.params);
});
auto depth = structurePass->depth;
/*
* create depth mipmap chain
*/
struct StructureMipmapData {
FrameGraphId<FrameGraphTexture> depth;
uint32_t rt[8];
};
fg.addPass<StructureMipmapData>("StructureMipmap",
[&](FrameGraph::Builder& builder, auto& data) {
data.depth = builder.sample(depth);
for (size_t i = 1; i < levelCount; i++) {
auto out = builder.createSubresource(data.depth, "Structure mip", {
.level = uint8_t(i)
});
out = builder.write(out, FrameGraphTexture::Usage::DEPTH_ATTACHMENT);
data.rt[i - 1] = builder.declareRenderPass("Structure mip target", {
.attachments = { .depth = out }
});
}
},
[=](FrameGraphResources const& resources, auto const& data, DriverApi& driver) {
auto in = resources.getTexture(data.depth);
// The first mip already exists, so we process n-1 lods
for (size_t level = 0; level < levelCount - 1; level++) {
auto out = resources.getRenderPassInfo(level);
driver.setMinMaxLevels(in, level, level);
auto& material = getPostProcessMaterial("mipmapDepth");
FMaterialInstance* const mi = material.getMaterialInstance();
mi->setParameter("depth", in, { .filterMin = SamplerMinFilter::NEAREST_MIPMAP_NEAREST });
mi->setParameter("level", uint32_t(level));
commitAndRender(out, material, driver);
}
driver.setMinMaxLevels(in, 0, levelCount - 1);
});
fg.getBlackboard().put("structure", depth);
fg.getBlackboard().put("picking", structurePass->picking);
return depth;
}
FrameGraphId<FrameGraphTexture> PostProcessManager::screenSpaceAmbientOcclusion(FrameGraph& fg,
filament::Viewport const& svp, const CameraInfo& cameraInfo,
AmbientOcclusionOptions const& options) noexcept {
FEngine& engine = mEngine;
Handle<HwRenderPrimitive> fullScreenRenderPrimitive = engine.getFullScreenRenderPrimitive();
FrameGraphId<FrameGraphTexture> depth = fg.getBlackboard().get<FrameGraphTexture>("structure");
assert_invariant(depth);
const size_t levelCount = fg.getDescriptor(depth).levels;
// With q the standard deviation,
// A gaussian filter requires 6q-1 values to keep its gaussian nature
// (see en.wikipedia.org/wiki/Gaussian_filter)
// More intuitively, 2q is the width of the filter in pixels.
BilateralPassConfig config = {
.bentNormals = options.bentNormals,
.bilateralThreshold = options.bilateralThreshold,
};
float sampleCount{};
float spiralTurns{};
float standardDeviation{};
switch (options.quality) {
default:
case QualityLevel::LOW:
sampleCount = 7.0f;
spiralTurns = 3.0f;
standardDeviation = 8.0;
break;
case QualityLevel::MEDIUM:
sampleCount = 11.0f;
spiralTurns = 6.0f;
standardDeviation = 8.0;
break;
case QualityLevel::HIGH:
sampleCount = 16.0f;
spiralTurns = 7.0f;
standardDeviation = 6.0;
break;
case QualityLevel::ULTRA:
sampleCount = 32.0f;
spiralTurns = 14.0f;
standardDeviation = 4.0;
break;
}
switch (options.lowPassFilter) {
default:
case QualityLevel::LOW:
// no filtering, values don't matter
config.kernelSize = 1;
config.standardDeviation = 1.0f;
config.scale = 1.0f;
break;
case QualityLevel::MEDIUM:
config.kernelSize = 11;
config.standardDeviation = standardDeviation * 0.5f;
config.scale = 2.0f;
break;
case QualityLevel::HIGH:
case QualityLevel::ULTRA:
config.kernelSize = 23;
config.standardDeviation = standardDeviation;
config.scale = 1.0f;
break;
}
// for debugging
//config.kernelSize = engine.debug.ssao.kernelSize;
//config.standardDeviation = engine.debug.ssao.stddev;
//sampleCount = engine.debug.ssao.sampleCount;
//spiralTurns = engine.debug.ssao.spiralTurns;
/*
* Our main SSAO pass
*/
struct SSAOPassData {
FrameGraphId<FrameGraphTexture> depth;
FrameGraphId<FrameGraphTexture> ssao;
FrameGraphId<FrameGraphTexture> ao;
FrameGraphId<FrameGraphTexture> bn;
};
const bool computeBentNormals = options.bentNormals;
const bool highQualityUpsampling =
options.upsampling >= QualityLevel::HIGH && options.resolution < 1.0f;
const bool lowPassFilterEnabled = options.lowPassFilter != QualityLevel::LOW;
auto& SSAOPass = fg.addPass<SSAOPassData>("SSAO Pass",
[&](FrameGraph::Builder& builder, auto& data) {
auto const& desc = builder.getDescriptor(depth);
data.depth = builder.sample(depth);
data.ssao = builder.createTexture("SSAO Buffer", {
.width = desc.width,
.height = desc.height,
.depth = computeBentNormals ? 2u : 1u,
.type = Texture::Sampler::SAMPLER_2D_ARRAY,
.format = (lowPassFilterEnabled || highQualityUpsampling || computeBentNormals) ?
TextureFormat::RGB8 : TextureFormat::R8
});
if (computeBentNormals) {
data.ao = builder.createSubresource(data.ssao, "SSAO attachment", { .layer = 0 });
data.bn = builder.createSubresource(data.ssao, "Bent Normals attachment", { .layer = 1 });
data.ao = builder.write(data.ao, FrameGraphTexture::Usage::COLOR_ATTACHMENT);
data.bn = builder.write(data.bn, FrameGraphTexture::Usage::COLOR_ATTACHMENT);
} else {
data.ao = data.ssao;
data.ao = builder.write(data.ao, FrameGraphTexture::Usage::COLOR_ATTACHMENT);
}
// Here we use the depth test to skip pixels at infinity (i.e. the skybox)
// Note that we have to clear the SAO buffer because blended objects will end-up
// reading into it even though they were not written in the depth buffer.
// The bilateral filter in the blur pass will ignore pixels at infinity.
data.depth = builder.read(data.depth, FrameGraphTexture::Usage::DEPTH_ATTACHMENT);
builder.declareRenderPass("SSAO Target", {
.attachments = { .color = { data.ao, data.bn }, .depth = data.depth },
.clearColor = { 1.0f },
.clearFlags = TargetBufferFlags::COLOR0 | TargetBufferFlags::COLOR1
});
},
[=](FrameGraphResources const& resources,
auto const& data, DriverApi& driver) {
auto depth = resources.getTexture(data.depth);
auto ssao = resources.getRenderPassInfo();
auto const& desc = resources.getDescriptor(data.depth);
// estimate of the size in pixel of a 1m tall/wide object viewed from 1m away (i.e. at z=-1)
const float projectionScale = std::min(
0.5f * cameraInfo.projection[0].x * desc.width,
0.5f * cameraInfo.projection[1].y * desc.height);
// Where the falloff function peaks
const float peak = 0.1f * options.radius;
const float intensity = (f::TAU * peak) * options.intensity;
// always square AO result, as it looks much better
const float power = options.power * 2.0f;
const auto invProjection = inverse(cameraInfo.projection);
const float inc = (1.0f / (sampleCount - 0.5f)) * spiralTurns * f::TAU;
const mat4 screenFromClipMatrix{ mat4::row_major_init{
0.5 * desc.width, 0.0, 0.0, 0.5 * desc.width,
0.0, 0.5 * desc.height, 0.0, 0.5 * desc.height,
0.0, 0.0, 0.5, 0.5,
0.0, 0.0, 0.0, 1.0
}};
auto& material = computeBentNormals ?
getPostProcessMaterial("saoBentNormals") :
getPostProcessMaterial("sao");
FMaterialInstance* const mi = material.getMaterialInstance();
mi->setParameter("depth", depth, {
.filterMin = SamplerMinFilter::NEAREST_MIPMAP_NEAREST });
mi->setParameter("screenFromViewMatrix",
mat4f(screenFromClipMatrix * cameraInfo.projection));
mi->setParameter("resolution",
float4{ desc.width, desc.height, 1.0f / desc.width, 1.0f / desc.height });
mi->setParameter("invRadiusSquared",
1.0f / (options.radius * options.radius));
mi->setParameter("minHorizonAngleSineSquared",
std::pow(std::sin(options.minHorizonAngleRad), 2.0f));
mi->setParameter("projectionScale",
projectionScale);
mi->setParameter("projectionScaleRadius",
projectionScale * options.radius);
mi->setParameter("positionParams", float2{
invProjection[0][0], invProjection[1][1] } * 2.0f);
mi->setParameter("peak2", peak * peak);
mi->setParameter("bias", options.bias);
mi->setParameter("power", power);
mi->setParameter("intensity", intensity / sampleCount);
mi->setParameter("maxLevel", uint32_t(levelCount - 1));
mi->setParameter("sampleCount", float2{ sampleCount, 1.0f / (sampleCount - 0.5f) });
mi->setParameter("spiralTurns", spiralTurns);
mi->setParameter("angleIncCosSin", float2{ std::cos(inc), std::sin(inc) });
mi->setParameter("invFarPlane", 1.0f / -cameraInfo.zf);
mi->setParameter("ssctShadowDistance", options.ssct.shadowDistance);
mi->setParameter("ssctConeAngleTangeant", std::tan(options.ssct.lightConeRad * 0.5f));
mi->setParameter("ssctContactDistanceMaxInv", 1.0f / options.ssct.contactDistanceMax);
// light direction in view space
// (note: this is actually equivalent to using the camera view matrix -- before the
// world matrix is accounted for)
auto m = cameraInfo.view * cameraInfo.worldOrigin;
const float3 l = normalize(
mat3f::getTransformForNormals(m.upperLeft())
* options.ssct.lightDirection);
mi->setParameter("ssctIntensity",
options.ssct.enabled ? options.ssct.intensity : 0.0f);
mi->setParameter("ssctVsLightDirection", -l);
mi->setParameter("ssctDepthBias",
float2{ options.ssct.depthBias, options.ssct.depthSlopeBias });
mi->setParameter("ssctSampleCount", uint32_t(options.ssct.sampleCount));
mi->setParameter("ssctRayCount",
float2{ options.ssct.rayCount, 1.0 / options.ssct.rayCount });
mi->commit(driver);
mi->use(driver);
PipelineState pipeline(material.getPipelineState());
pipeline.rasterState.depthFunc = RasterState::DepthFunc::L;
driver.beginRenderPass(ssao.target, ssao.params);
driver.draw(pipeline, fullScreenRenderPrimitive);
driver.endRenderPass();
});
FrameGraphId<FrameGraphTexture> ssao = SSAOPass->ssao;
/*
* Final separable bilateral blur pass
*/
if (lowPassFilterEnabled) {
ssao = bilateralBlurPass(fg, ssao, { config.scale, 0 }, cameraInfo.zf,
TextureFormat::RGB8,
config);
ssao = bilateralBlurPass(fg, ssao, { 0, config.scale }, cameraInfo.zf,
(highQualityUpsampling || computeBentNormals) ? TextureFormat::RGB8 : TextureFormat::R8,
config);
}
fg.getBlackboard().put("ssao", ssao);
return ssao;
}
FrameGraphId<FrameGraphTexture> PostProcessManager::bilateralBlurPass(
FrameGraph& fg, FrameGraphId<FrameGraphTexture> input, math::int2 axis, float zf,
TextureFormat format, BilateralPassConfig config) noexcept {
Handle<HwRenderPrimitive> fullScreenRenderPrimitive = mEngine.getFullScreenRenderPrimitive();
struct BlurPassData {
FrameGraphId<FrameGraphTexture> input;
FrameGraphId<FrameGraphTexture> blurred;
FrameGraphId<FrameGraphTexture> ao;
FrameGraphId<FrameGraphTexture> bn;
};
auto& blurPass = fg.addPass<BlurPassData>("Separable Blur Pass",
[&](FrameGraph::Builder& builder, auto& data) {
auto const& desc = builder.getDescriptor(input);
data.input = builder.sample(input);
data.blurred = builder.createTexture("Blurred output", {
.width = desc.width,
.height = desc.height,
.depth = desc.depth,
.type = desc.type,
.format = format });
FrameGraphId<FrameGraphTexture> depth = fg.getBlackboard().get<FrameGraphTexture>("structure");
assert_invariant(depth);
depth = builder.read(depth, FrameGraphTexture::Usage::DEPTH_ATTACHMENT);
if (config.bentNormals) {
data.ao = builder.createSubresource(data.blurred, "SSAO attachment", { .layer = 0 });
data.bn = builder.createSubresource(data.blurred, "Bent Normals attachment", { .layer = 1 });
data.ao = builder.write(data.ao, FrameGraphTexture::Usage::COLOR_ATTACHMENT);
data.bn = builder.write(data.bn, FrameGraphTexture::Usage::COLOR_ATTACHMENT);
} else {
data.ao = data.blurred;
data.ao = builder.write(data.ao, FrameGraphTexture::Usage::COLOR_ATTACHMENT);
}
// Here we use the depth test to skip pixels at infinity (i.e. the skybox)
// We need to clear the buffers because we are skipping pixels at infinity (skybox)
data.blurred = builder.write(data.blurred, FrameGraphTexture::Usage::COLOR_ATTACHMENT);
builder.declareRenderPass("Blurred target", {
.attachments = { .color = { data.ao, data.bn }, .depth = depth },
.clearColor = { 1.0f },
.clearFlags = TargetBufferFlags::COLOR0 | TargetBufferFlags::COLOR1
});
},
[=](FrameGraphResources const& resources,
auto const& data, DriverApi& driver) {
auto ssao = resources.getTexture(data.input);
auto blurred = resources.getRenderPassInfo();
auto const& desc = resources.getDescriptor(data.blurred);
// unnormalized gaussian half-kernel of a given standard deviation
// returns number of samples stored in array (max 16)
constexpr size_t kernelArraySize = 16; // limited by bilateralBlur.mat
auto gaussianKernel =
[kernelArraySize](float* outKernel, size_t gaussianWidth, float stdDev) -> uint32_t {
const size_t gaussianSampleCount = std::min(kernelArraySize, (gaussianWidth + 1u) / 2u);
for (size_t i = 0; i < gaussianSampleCount; i++) {
float x = i;
float g = std::exp(-(x * x) / (2.0f * stdDev * stdDev));
outKernel[i] = g;
}
return uint32_t(gaussianSampleCount);
};
float kGaussianSamples[kernelArraySize];
uint32_t kGaussianCount = gaussianKernel(kGaussianSamples,
config.kernelSize, config.standardDeviation);
auto& material = config.bentNormals ?
getPostProcessMaterial("bilateralBlurBentNormals") :
getPostProcessMaterial("bilateralBlur");
FMaterialInstance* const mi = material.getMaterialInstance();
mi->setParameter("ssao", ssao, { /* only reads level 0 */ });
mi->setParameter("axis", axis / float2{desc.width, desc.height});
mi->setParameter("kernel", kGaussianSamples, kGaussianCount);
mi->setParameter("sampleCount", kGaussianCount);
mi->setParameter("farPlaneOverEdgeDistance", -zf / config.bilateralThreshold);
mi->commit(driver);
mi->use(driver);
PipelineState pipeline(material.getPipelineState());
pipeline.rasterState.depthFunc = RasterState::DepthFunc::L;
driver.beginRenderPass(blurred.target, blurred.params);
driver.draw(pipeline, fullScreenRenderPrimitive);
driver.endRenderPass();
});
return blurPass->blurred;
}
FrameGraphId<FrameGraphTexture> PostProcessManager::generateGaussianMipmap(FrameGraph& fg,
FrameGraphId<FrameGraphTexture> 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, 0, reinhard, kernelWidth, sigmaRatio);
reinhard = false; // only do the reinhard filtering on the first level
}
return input;
}
FrameGraphId<FrameGraphTexture> PostProcessManager::gaussianBlurPass(FrameGraph& fg,
FrameGraphId<FrameGraphTexture> input, uint8_t srcLevel,
FrameGraphId<FrameGraphTexture> output, uint8_t dstLevel, uint8_t layer,
bool reinhard, size_t kernelWidth, float sigmaRatio) noexcept {
const float sigma = (kernelWidth + 1.0f) / sigmaRatio;
Handle<HwRenderPrimitive> fullScreenRenderPrimitive = mEngine.getFullScreenRenderPrimitive();
auto computeGaussianCoefficients = [kernelWidth, sigma](float2* kernel, size_t size) -> size_t {
const float alpha = 1.0f / (2.0f * sigma * sigma);
// number of positive-side samples needed, using linear sampling
size_t m = (kernelWidth - 1) / 4 + 1;
// clamp to what we have
m = std::min(size, m);
// How the kernel samples are stored:
// *===*---+---+---+---+---+---+
// | 0 | 1 | 2 | 3 | 4 | 5 | 6 | Gaussian coefficients (right size)
// *===*-------+-------+-------+
// | 0 | 1 | 2 | 3 | stored coefficients (right side)
kernel[0].x = 1.0;
kernel[0].y = 0.0;
float totalWeight = kernel[0].x;
for (size_t i = 1; i < m; i++) {
float x0 = i * 2 - 1;
float x1 = i * 2;
float k0 = std::exp(-alpha * x0 * x0);
float k1 = std::exp(-alpha * x1 * x1);
float k = k0 + k1;
float o = k0 / k;
kernel[i].x = k;
kernel[i].y = o;
totalWeight += (k0 + k1) * 2.0f;
}
for (size_t i = 0; i < m; i++) {
kernel[i].x *= 1.0f / totalWeight;
}
return m;
};
struct BlurPassData {
FrameGraphId<FrameGraphTexture> in;
FrameGraphId<FrameGraphTexture> out;
FrameGraphId<FrameGraphTexture> temp;
};
// 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<BlurPassData>("Gaussian Blur Passes",
[&](FrameGraph::Builder& builder, auto& data) {
auto desc = builder.getDescriptor(input);
if (!output) {
output = builder.createTexture("Blurred texture", desc);
}
data.in = builder.sample(input);
// width of the destination level (b/c we're blurring horizontally)
desc.width = FTexture::valueForLevel(dstLevel, desc.width);
// height of the source level (b/c it's not blurred in this pass)
desc.height = FTexture::valueForLevel(srcLevel, desc.height);
// only one level
desc.levels = 1;
data.temp = builder.createTexture("Horizontal temporary buffer", desc);
data.temp = builder.sample(data.temp);
data.temp = builder.declareRenderPass(data.temp);
data.out = builder.createSubresource(output, "Blurred texture mip",
{ .level = dstLevel, .layer = layer });
data.out = builder.declareRenderPass(data.out);
},
[=](FrameGraphResources const& resources,
auto const& data, DriverApi& driver) {
auto hwTempRT = resources.getRenderPassInfo(0);
auto hwOutRT = resources.getRenderPassInfo(1);
auto hwTemp = resources.getTexture(data.temp);
auto hwIn = resources.getTexture(data.in);
auto const& inDesc = resources.getDescriptor(data.in);
auto const& outDesc = resources.getDescriptor(data.out);
auto const& tempDesc = resources.getDescriptor(data.temp);
utils::StaticString materialName;
switch (backend::getFormatSize(outDesc.format)) {
case 1: materialName = "separableGaussianBlur1"; break;
case 2: materialName = "separableGaussianBlur2"; break;
case 3: materialName = "separableGaussianBlur3"; break;
default: materialName = "separableGaussianBlur4"; break;
}
auto const& separableGaussianBlur = getPostProcessMaterial(materialName);
FMaterialInstance* const mi = separableGaussianBlur.getMaterialInstance();
const size_t kernelStorageSize = mi->getMaterial()->reflect("kernel")->size;
float2 kernel[64];
size_t m = computeGaussianCoefficients(kernel,
std::min(sizeof(kernel) / sizeof(*kernel), kernelStorageSize));
// horizontal pass
mi->setParameter("source", hwIn, {
.filterMag = SamplerMagFilter::LINEAR,
.filterMin = SamplerMinFilter::LINEAR_MIPMAP_NEAREST
});
mi->setParameter("level", (float)srcLevel);
mi->setParameter("reinhard", reinhard ? uint32_t(1) : uint32_t(0));
mi->setParameter("resolution", float4{
tempDesc.width, tempDesc.height,
1.0f / tempDesc.width, 1.0f / tempDesc.height });
mi->setParameter("axis",
float2{ 1.0f / FTexture::valueForLevel(srcLevel, inDesc.width), 0 });
mi->setParameter("count", (int32_t)m);
mi->setParameter("kernel", kernel, m);
// The framegraph only computes discard flags at FrameGraphPass boundaries
hwTempRT.params.flags.discardEnd = TargetBufferFlags::NONE;
commitAndRender(hwTempRT, separableGaussianBlur, driver);
// vertical pass
auto width = outDesc.width;
auto height = outDesc.height;
assert_invariant(width == hwOutRT.params.viewport.width);
assert_invariant(height == hwOutRT.params.viewport.height);
mi->setParameter("source", hwTemp, {
.filterMag = SamplerMagFilter::LINEAR,
.filterMin = SamplerMinFilter::LINEAR /* level is always 0 */
});
mi->setParameter("level", 0.0f);
mi->setParameter("resolution",
float4{ width, height, 1.0f / width, 1.0f / height });
mi->setParameter("axis", float2{ 0, 1.0f / tempDesc.height });
mi->commit(driver);
// we don't need to call use() here, since it's the same material
driver.beginRenderPass(hwOutRT.target, hwOutRT.params);
driver.draw(separableGaussianBlur.getPipelineState(), fullScreenRenderPrimitive);
driver.endRenderPass();
});
return output;
}
FrameGraphId<FrameGraphTexture> PostProcessManager::dof(FrameGraph& fg,
FrameGraphId<FrameGraphTexture> input, const DepthOfFieldOptions& dofOptions,
bool translucent, const CameraInfo& cameraInfo, float2 scale) noexcept {
FEngine& engine = mEngine;
Handle<HwRenderPrimitive> const& fullScreenRenderPrimitive = engine.getFullScreenRenderPrimitive();
const uint8_t variant = uint8_t(
translucent ? PostProcessVariant::TRANSLUCENT : PostProcessVariant::OPAQUE);
const TextureFormat format = translucent ? TextureFormat::RGBA16F
: TextureFormat::R11F_G11F_B10F;
// rotate the bokeh based on the aperture diameter (i.e. angle of the blades)
float bokehAngle = f::PI / 6.0f;
if (dofOptions.maxApertureDiameter > 0.0f) {
bokehAngle += f::PI_2 * saturate(cameraInfo.A / dofOptions.maxApertureDiameter);
}
/*
* Circle-of-confusion
* -------------------
*
* (see https://en.wikipedia.org/wiki/Circle_of_confusion)
*
* Ap: aperture [m]
* f: focal length [m]
* S: focus distance [m]
* d: distance to the focal plane [m]
*
* f f | S |
* coc(d) = --- . ----- . | 1 - --- | in meters (m)
* Ap S - f | d |
*
* This can be rewritten as:
*
* coc(z) = Kc . Ks . (1 - S / d) in pixels [px]
*
* A.f
* Kc = ----- with: A = f / Ap
* S - f
*
* Ks = height [px] / SensorSize [m] pixel conversion
*
*
* We also introduce a "cocScale" factor for artistic reasons (see code below).
*
*
* Object distance computation (d)
* -------------------------------
*
* 1/d is computed from the depth buffer value as:
* (note: our Z clip space is 1 to 0 (inverted DirectX NDC))
*
* screen-space -> clip-space -> view-space -> distance (*-1)
*
* v_s = { x, y, z, 1 } // screen space (reversed-z)
* v_c = v_s // clip space (matches screen space)
* v = inverse(projection) * v_c // view space
* d = -v.z / v.w // view space distance to camera
* 1/d = -v.w / v.z
*
* Assuming a generic projection matrix of the form:
*
* a 0 x 0
* 0 b y 0
* 0 0 A B
* 0 0 C 0
*
* It comes that:
*
* C A
* 1/d = --- . z - ---
* B B
*
* note: Here the result doesn't depend on {x, y}. This wouldn't be the case with a
* tilt-shift lens.
*
* Mathematica code:
* p = {{a, 0, b, 0}, {0, c, d, 0}, {0, 0, m22, m32}, {0, 0, m23, 0}};
* v = {x, y, z, 1};
* f = Inverse[p].v;
* Simplify[f[[4]]/f[[3]]]
*
* Plugging this back into the expression of: coc(z) = Kc . Ks . (1 - S / d)
* We get that: coc(z) = C0 * z + C1
* With: C0 = - Kc * Ks * S * -C / B
* C1 = Kc * Ks * (1 + S * A / B)
*
* It's just a madd!
*/
const float focusDistance = cameraInfo.d;
auto const& desc = fg.getDescriptor<FrameGraphTexture>(input);
const float Kc = (cameraInfo.A * cameraInfo.f) / (focusDistance - cameraInfo.f);
const float Ks = ((float)desc.height) / FCamera::SENSOR_SIZE;
const float K = dofOptions.cocScale * Ks * Kc;
auto const& p = cameraInfo.projection;
const float2 cocParams = {
K * focusDistance * p[2][3] / p[3][2],
K * (1.0 + focusDistance * p[2][2] / p[3][2])
};
Blackboard& blackboard = fg.getBlackboard();
auto depth = blackboard.get<FrameGraphTexture>("depth");
assert_invariant(depth);
/*
* dofResolution is used (at compile time for now) to chose between full- or quarter-resolution
* for the DoF calculations. Set to [1] for full resolution or [2] for quarter-resolution.
*/
const uint32_t dofResolution = dofOptions.nativeResolution ? 1u : 2u;
constexpr const uint32_t maxMipLevels = 4u; // mip levels at full-resolution
constexpr const uint32_t maxMipLevelsMask = (1u << maxMipLevels) - 1u;
auto const& colorDesc = fg.getDescriptor(input);
const uint32_t width = ((colorDesc.width + maxMipLevelsMask) & ~maxMipLevelsMask) / dofResolution;
const uint32_t height = ((colorDesc.height + maxMipLevelsMask) & ~maxMipLevelsMask) / dofResolution;
const uint8_t maxLevelCount = FTexture::maxLevelCount(width, height);
uint8_t mipmapCount = min(maxLevelCount, uint8_t(maxMipLevels));
/*
* Setup:
* - Downsample of color buffer
* - Separate near & far field
* - Generate Circle Of Confusion buffer
*/
struct PostProcessDofDownsample {
FrameGraphId<FrameGraphTexture> color;
FrameGraphId<FrameGraphTexture> depth;
FrameGraphId<FrameGraphTexture> outColor;
FrameGraphId<FrameGraphTexture> outCoc;
};
auto& ppDoFDownsample = fg.addPass<PostProcessDofDownsample>("DoF Downsample",
[&](FrameGraph::Builder& builder, auto& data) {
data.color = builder.sample(input);
data.depth = builder.sample(depth);
data.outColor = builder.createTexture("dof downsample output", {
.width = width, .height = height, .levels = mipmapCount, .format = format
});
data.outCoc = builder.createTexture("dof CoC output", {
.width = width, .height = height, .levels = mipmapCount,
.format = TextureFormat::R16F,
.swizzle = {
// the next stage expects min/max CoC in the red/green channel
.r = backend::TextureSwizzle::CHANNEL_0,
.g = backend::TextureSwizzle::CHANNEL_0 },
});
data.outColor = builder.write(data.outColor, FrameGraphTexture::Usage::COLOR_ATTACHMENT);
data.outCoc = builder.write(data.outCoc, FrameGraphTexture::Usage::COLOR_ATTACHMENT);
builder.declareRenderPass("DoF Target", { .attachments = {
.color = { data.outColor, data.outCoc }
}
});
},
[=](FrameGraphResources const& resources, auto const& data, DriverApi& driver) {
auto const& out = resources.getRenderPassInfo();
auto color = resources.getTexture(data.color);
auto depth = resources.getTexture(data.depth);
auto const& material = (dofResolution == 1) ?
getPostProcessMaterial("dofCoc") :
getPostProcessMaterial("dofDownsample");
FMaterialInstance* const mi = material.getMaterialInstance();
mi->setParameter("color", color, { .filterMin = SamplerMinFilter::NEAREST });
mi->setParameter("depth", depth, { .filterMin = SamplerMinFilter::NEAREST });
mi->setParameter("cocParams", cocParams);
mi->setParameter("cocClamp", float2{
-(dofOptions.maxForegroundCOC ? dofOptions.maxForegroundCOC : DOF_DEFAULT_MAX_COC),
dofOptions.maxBackgroundCOC ? dofOptions.maxBackgroundCOC : DOF_DEFAULT_MAX_COC});
mi->setParameter("uvscale", float4{ width, height,
1.0f / colorDesc.width, 1.0f / colorDesc.height });
commitAndRender(out, material, driver);
});
/*
* Setup (Continued)
* - Generate mipmaps
*/
struct PostProcessDofMipmap {
FrameGraphId<FrameGraphTexture> inOutColor;
FrameGraphId<FrameGraphTexture> inOutCoc;
uint32_t rp[maxMipLevels];
};
assert_invariant(mipmapCount - 1 <= sizeof(PostProcessDofMipmap::rp) / sizeof(uint32_t));
auto& ppDoFMipmap = fg.addPass<PostProcessDofMipmap>("DoF Mipmap",
[&](FrameGraph::Builder& builder, auto& data) {
data.inOutColor = builder.sample(ppDoFDownsample->outColor);
data.inOutCoc = builder.sample(ppDoFDownsample->outCoc);
for (size_t i = 0; i < mipmapCount - 1u; i++) {
// make sure inputs are always multiple of two (should be true by construction)
// (this is so that we can compute clean mip levels)
assert_invariant((FTexture::valueForLevel(uint8_t(i), fg.getDescriptor(data.inOutColor).width ) & 0x1u) == 0);
assert_invariant((FTexture::valueForLevel(uint8_t(i), fg.getDescriptor(data.inOutColor).height) & 0x1u) == 0);
auto inOutColor = builder.createSubresource(data.inOutColor, "Color mip", { .level = uint8_t(i + 1) });
auto inOutCoc = builder.createSubresource(data.inOutCoc, "Coc mip", { .level = uint8_t(i + 1) });
inOutColor = builder.write(inOutColor, FrameGraphTexture::Usage::COLOR_ATTACHMENT);
inOutCoc = builder.write(inOutCoc, FrameGraphTexture::Usage::COLOR_ATTACHMENT);
data.rp[i] = builder.declareRenderPass("DoF Target", { .attachments = {
.color = { inOutColor, inOutCoc }
}
});
}
},
[=](FrameGraphResources const& resources,
auto const& data, DriverApi& driver) {
auto desc = resources.getDescriptor(data.inOutColor);
auto inOutColor = resources.getTexture(data.inOutColor);
auto inOutCoc = resources.getTexture(data.inOutCoc);
auto const& material = getPostProcessMaterial("dofMipmap");
FMaterialInstance* const mi = material.getMaterialInstance();
mi->setParameter("color", inOutColor, { .filterMin = SamplerMinFilter::NEAREST_MIPMAP_NEAREST });
mi->setParameter("coc", inOutCoc, { .filterMin = SamplerMinFilter::NEAREST_MIPMAP_NEAREST });
mi->use(driver);
const PipelineState pipeline(material.getPipelineState(variant));
for (size_t level = 0 ; level < mipmapCount - 1u ; level++) {
uint32_t w = FTexture::valueForLevel(level, desc.width);
uint32_t h = FTexture::valueForLevel(level, desc.height);
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("pixelSize", 1.0f / float2{w, h});
mi->commit(driver);
driver.beginRenderPass(out.target, out.params);
driver.draw(pipeline, fullScreenRenderPrimitive);
driver.endRenderPass();
}
driver.setMinMaxLevels(inOutColor, 0, mipmapCount - 1u);
driver.setMinMaxLevels(inOutCoc, 0, mipmapCount - 1u);
});
/*
* Setup (Continued)
* - Generate min/max tiles for far/near fields (continued)
*/
auto inTilesCocMinMax = ppDoFDownsample->outCoc;
// TODO: Should the tile size be in real pixels? i.e. always 16px instead of being dependant on
// the DoF effect resolution?
// Size of a tile in full-resolution pixels -- must match TILE_SIZE in dofDilate.mat
const size_t tileSize = 16;
// round the width/height to 16 (tile size), divide by scale (1 or 2) for halfres
const uint32_t tileBufferWidth = ((colorDesc.width + (tileSize - 1u)) & ~(tileSize - 1u)) / dofResolution;
const uint32_t tileBufferHeight = ((colorDesc.height + (tileSize - 1u)) & ~(tileSize - 1u)) / dofResolution;
const size_t tileReductionCount = std::log2(tileSize / dofResolution);
struct PostProcessDofTiling1 {
FrameGraphId<FrameGraphTexture> inCocMinMax;
FrameGraphId<FrameGraphTexture> outTilesCocMinMax;
};
const bool textureSwizzleSupported = Texture::isTextureSwizzleSupported(mEngine);
for (size_t i = 0; i < tileReductionCount; i++) {
auto& ppDoFTiling = fg.addPass<PostProcessDofTiling1>("DoF Tiling",
[&](FrameGraph::Builder& builder, auto& data) {
assert_invariant(((tileBufferWidth >> i) & 1u) == 0);
assert_invariant(((tileBufferHeight >> i) & 1u) == 0);
data.inCocMinMax = builder.sample(inTilesCocMinMax);
data.outTilesCocMinMax = builder.createTexture("dof tiles output", {
.width = tileBufferWidth >> (i + 1u),
.height = tileBufferHeight >> (i + 1u),
.format = TextureFormat::RG16F
});
data.outTilesCocMinMax = builder.declareRenderPass(data.outTilesCocMinMax);
},
[=](FrameGraphResources const& resources,
auto const& data, DriverApi& driver) {
auto const& inputDesc = resources.getDescriptor(data.inCocMinMax);
auto const& outputDesc = resources.getDescriptor(data.outTilesCocMinMax);
auto const& out = resources.getRenderPassInfo();
auto inCocMinMax = resources.getTexture(data.inCocMinMax);
auto const& material = (!textureSwizzleSupported && (i == 0)) ?
getPostProcessMaterial("dofTilesSwizzle") :
getPostProcessMaterial("dofTiles");
FMaterialInstance* const mi = material.getMaterialInstance();
mi->setParameter("cocMinMax", inCocMinMax, { .filterMin = SamplerMinFilter::NEAREST });
mi->setParameter("uvscale", float4{
outputDesc.width, outputDesc.height,
1.0f / inputDesc.width, 1.0f / inputDesc.height });
commitAndRender(out, material, driver);
});
inTilesCocMinMax = ppDoFTiling->outTilesCocMinMax;
}
/*
* Dilate tiles
*/
// This is a small helper that does one round of dilate
auto dilate = [&](FrameGraphId<FrameGraphTexture> input) -> FrameGraphId<FrameGraphTexture> {
struct PostProcessDofDilate {
FrameGraphId<FrameGraphTexture> inTilesCocMinMax;
FrameGraphId<FrameGraphTexture> outTilesCocMinMax;
};
auto& ppDoFDilate = fg.addPass<PostProcessDofDilate>("DoF Dilate",
[&](FrameGraph::Builder& builder, auto& data) {
auto const& inputDesc = fg.getDescriptor(input);
data.inTilesCocMinMax = builder.sample(input);
data.outTilesCocMinMax = builder.createTexture("dof dilated tiles output", inputDesc);
data.outTilesCocMinMax = builder.declareRenderPass(data.outTilesCocMinMax );
},
[=](FrameGraphResources const& resources,
auto const& data, DriverApi& driver) {
auto const& out = resources.getRenderPassInfo();
auto inTilesCocMinMax = resources.getTexture(data.inTilesCocMinMax);
auto const& material = getPostProcessMaterial("dofDilate");
FMaterialInstance* const mi = material.getMaterialInstance();
mi->setParameter("tiles", inTilesCocMinMax, { .filterMin = SamplerMinFilter::NEAREST });
commitAndRender(out, material, driver);
});
return ppDoFDilate->outTilesCocMinMax;
};
// Tiles of 16 full-resolution pixels requires two dilate rounds to accommodate our max Coc of 32 pixels
// (note: when running at half-res, the tiles are 8 half-resolution pixels, and still need two
// dilate rounds to accommodate the mac CoC pf 16 half-resolution pixels)
auto dilated = dilate(inTilesCocMinMax);
dilated = dilate(dilated);
/*
* DoF blur pass
*/
struct PostProcessDof {
FrameGraphId<FrameGraphTexture> color;
FrameGraphId<FrameGraphTexture> coc;
FrameGraphId<FrameGraphTexture> tilesCocMinMax;
FrameGraphId<FrameGraphTexture> outColor;
FrameGraphId<FrameGraphTexture> outAlpha;
};
auto& ppDoF = fg.addPass<PostProcessDof>("DoF",
[&](FrameGraph::Builder& builder, auto& data) {
data.color = builder.sample(ppDoFMipmap->inOutColor);
data.coc = builder.sample(ppDoFMipmap->inOutCoc);
data.tilesCocMinMax = builder.sample(dilated);
// The DoF buffer (output) doesn't need to be a multiple of 8 because it's not
// mipmapped. We just need to adjust the uv properly.
data.outColor = builder.createTexture("dof color output", {
.width = (colorDesc.width + (dofResolution / 2u)) / dofResolution,
.height = (colorDesc.height + (dofResolution / 2u)) / dofResolution,
.format = fg.getDescriptor(data.color).format
});
data.outAlpha = builder.createTexture("dof alpha output", {
.width = builder.getDescriptor(data.outColor).width,
.height = builder.getDescriptor(data.outColor).height,
.format = TextureFormat::R8
});
data.outColor = builder.write(data.outColor, FrameGraphTexture::Usage::COLOR_ATTACHMENT);
data.outAlpha = builder.write(data.outAlpha, FrameGraphTexture::Usage::COLOR_ATTACHMENT);
builder.declareRenderPass("DoF Target", {
.attachments = { .color = { data.outColor, data.outAlpha }}
});
},
[=](FrameGraphResources const& resources, auto const& data, DriverApi& driver) {
auto const& out = resources.getRenderPassInfo();
auto color = resources.getTexture(data.color);
auto coc = resources.getTexture(data.coc);
auto tilesCocMinMax = resources.getTexture(data.tilesCocMinMax);
auto const& inputDesc = resources.getDescriptor(data.coc);
auto const& outputDesc = resources.getDescriptor(data.outColor);
auto const& tilesDesc = resources.getDescriptor(data.tilesCocMinMax);
auto const& material = getPostProcessMaterial("dof");
FMaterialInstance* const mi = material.getMaterialInstance();
// it's not safe to use bilinear filtering in the general case (causes artifacts around edges)
mi->setParameter("color", color,
{ .filterMin = SamplerMinFilter::NEAREST_MIPMAP_NEAREST });
mi->setParameter("colorLinear", color,
{ .filterMin = SamplerMinFilter::LINEAR_MIPMAP_NEAREST });
mi->setParameter("coc", coc,
{ .filterMin = SamplerMinFilter::NEAREST_MIPMAP_NEAREST });
mi->setParameter("tiles", tilesCocMinMax,
{ .filterMin = SamplerMinFilter::NEAREST });
// The bokeh height is always correct regardless of the dynamic resolution scaling.
// (because the CoC is calculated w.r.t. the height), so we only need to adjust
// the width.
const float aspectRatio = scale.x / scale.y;
mi->setParameter("cocToTexelScale", float2{
aspectRatio / (inputDesc.width * dofResolution),
1.0 / (inputDesc.height * dofResolution)
});
mi->setParameter("cocToPixelScale", (1.0f / dofResolution));
mi->setParameter("uvscale", float4{
outputDesc.width / float(inputDesc.width),
outputDesc.height / float(inputDesc.height),
outputDesc.width / float(tileSize / dofResolution * tilesDesc.width),
outputDesc.height / float(tileSize / dofResolution * tilesDesc.height)
});
mi->setParameter("ringCounts", float4{
dofOptions.foregroundRingCount ? dofOptions.foregroundRingCount : DOF_DEFAULT_RING_COUNT,
dofOptions.backgroundRingCount ? dofOptions.backgroundRingCount : DOF_DEFAULT_RING_COUNT,
dofOptions.fastGatherRingCount ? dofOptions.fastGatherRingCount : DOF_DEFAULT_RING_COUNT,
0.0 // unused for now
});
mi->setParameter("bokehAngle", bokehAngle);
commitAndRender(out, material, driver);
});
/*
* DoF median
*/
struct PostProcessDofMedian {
FrameGraphId<FrameGraphTexture> inColor;
FrameGraphId<FrameGraphTexture> inAlpha;
FrameGraphId<FrameGraphTexture> tilesCocMinMax;
FrameGraphId<FrameGraphTexture> outColor;
FrameGraphId<FrameGraphTexture> outAlpha;
};
auto& ppDoFMedian = fg.addPass<PostProcessDofMedian>("DoF Median",
[&](FrameGraph::Builder& builder, auto& data) {
data.inColor = builder.sample(ppDoF->outColor);
data.inAlpha = builder.sample(ppDoF->outAlpha);
data.tilesCocMinMax = builder.sample(dilated);
data.outColor = builder.createTexture("dof color output", fg.getDescriptor(data.inColor));
data.outAlpha = builder.createTexture("dof alpha output", fg.getDescriptor(data.inAlpha));
data.outColor = builder.write(data.outColor, FrameGraphTexture::Usage::COLOR_ATTACHMENT);
data.outAlpha = builder.write(data.outAlpha, FrameGraphTexture::Usage::COLOR_ATTACHMENT);
builder.declareRenderPass("DoF Target", {
.attachments = { .color = { data.outColor, data.outAlpha }}
});
},
[=](FrameGraphResources const& resources, auto const& data, DriverApi& driver) {
auto const& out = resources.getRenderPassInfo();
auto inColor = resources.getTexture(data.inColor);
auto inAlpha = resources.getTexture(data.inAlpha);
auto tilesCocMinMax = resources.getTexture(data.tilesCocMinMax);
auto const& outputDesc = resources.getDescriptor(data.outColor);
auto const& tilesDesc = resources.getDescriptor(data.tilesCocMinMax);
auto const& material = getPostProcessMaterial("dofMedian");
FMaterialInstance* const mi = material.getMaterialInstance();
mi->setParameter("dof", inColor, { .filterMin = SamplerMinFilter::NEAREST_MIPMAP_NEAREST });
mi->setParameter("alpha", inAlpha, { .filterMin = SamplerMinFilter::NEAREST_MIPMAP_NEAREST });
mi->setParameter("tiles", tilesCocMinMax, { .filterMin = SamplerMinFilter::NEAREST });
mi->setParameter("uvscale", float2{
outputDesc.width / float(tileSize / dofResolution * tilesDesc.width),
outputDesc.height / float(tileSize / dofResolution * tilesDesc.height)
});
commitAndRender(out, material, driver);
});
/*
* DoF recombine
*/
auto outColor = ppDoFMedian->outColor;
auto outAlpha = ppDoFMedian->outAlpha;
if (dofOptions.filter == DepthOfFieldOptions::Filter::NONE) {
outColor = ppDoF->outColor;
outAlpha = ppDoF->outAlpha;
}
struct PostProcessDofCombine {
FrameGraphId<FrameGraphTexture> color;
FrameGraphId<FrameGraphTexture> dof;
FrameGraphId<FrameGraphTexture> alpha;
FrameGraphId<FrameGraphTexture> tilesCocMinMax;
FrameGraphId<FrameGraphTexture> output;
};
auto& ppDoFCombine = fg.addPass<PostProcessDofCombine>("DoF combine",
[&](FrameGraph::Builder& builder, auto& data) {
data.color = builder.sample(input);
data.dof = builder.sample(outColor);
data.alpha = builder.sample(outAlpha);
data.tilesCocMinMax = builder.sample(dilated);
auto const& inputDesc = fg.getDescriptor(data.color);
data.output = builder.createTexture("DoF output", inputDesc);
data.output = builder.declareRenderPass(data.output);
},
[=](FrameGraphResources const& resources,
auto const& data, DriverApi& driver) {
auto const& dofDesc = resources.getDescriptor(data.dof);
auto const& tilesDesc = resources.getDescriptor(data.tilesCocMinMax);
auto const& out = resources.getRenderPassInfo();
auto color = resources.getTexture(data.color);
auto dof = resources.getTexture(data.dof);
auto alpha = resources.getTexture(data.alpha);
auto tilesCocMinMax = resources.getTexture(data.tilesCocMinMax);
auto const& material = getPostProcessMaterial("dofCombine");
FMaterialInstance* const mi = material.getMaterialInstance();
mi->setParameter("color", color, { .filterMin = SamplerMinFilter::NEAREST });
mi->setParameter("dof", dof, { .filterMag = SamplerMagFilter::NEAREST });
mi->setParameter("alpha", alpha, { .filterMag = SamplerMagFilter::NEAREST });
mi->setParameter("tiles", tilesCocMinMax, { .filterMin = SamplerMinFilter::NEAREST });
mi->setParameter("uvscale", float4{
colorDesc.width / (dofDesc.width * float(dofResolution)),
colorDesc.height / (dofDesc.height * float(dofResolution)),
colorDesc.width / (tilesDesc.width * float(tileSize)),
colorDesc.height / (tilesDesc.height * float(tileSize))
});
commitAndRender(out, material, driver);
});
return ppDoFCombine->output;
}
FrameGraphId<FrameGraphTexture> PostProcessManager::bloom(FrameGraph& fg,
FrameGraphId<FrameGraphTexture> input,
BloomOptions& inoutBloomOptions, backend::TextureFormat outFormat,
math::float2 scale) noexcept {
FrameGraphId<FrameGraphTexture> bloom = bloomPass(fg, input,
outFormat, inoutBloomOptions, scale);
fg.getBlackboard().put("bloom", bloom);
return bloom;
}
FrameGraphId<FrameGraphTexture> PostProcessManager::bloomPass(FrameGraph& fg,
FrameGraphId<FrameGraphTexture> input, TextureFormat outFormat,
BloomOptions& inoutBloomOptions, float2 scale) noexcept {
// Chrome does not support feedback loops in WebGL 2.0. See also:
// https://bugs.chromium.org/p/chromium/issues/detail?id=1066201
#if defined(__EMSCRIPTEN__)
constexpr bool isWebGL = true;
#else
constexpr bool isWebGL = false;
#endif
Handle<HwRenderPrimitive> fullScreenRenderPrimitive = mEngine.getFullScreenRenderPrimitive();
// Figure out a good size for the bloom buffer.
auto const& desc = fg.getDescriptor(input);
// width and height after dynamic resolution upscaling
const float aspect = (desc.width * scale.y) / (desc.height * scale.x);
// compute the desired bloom buffer size
float bloomHeight = inoutBloomOptions.resolution;
float bloomWidth = bloomHeight * aspect;
// Anamorphic bloom by always scaling down one of the dimension -- we do this (as opposed
// to scaling up) so that the amount of blooming doesn't decrease. However, the resolution
// decreases, meaning that the user might need to adjust the BloomOptions::resolution and
// BloomOptions::levels.
if (inoutBloomOptions.anamorphism >= 1.0f) {
bloomWidth *= 1.0f / inoutBloomOptions.anamorphism;
} else {
bloomHeight *= inoutBloomOptions.anamorphism;
}
// convert back to integer width/height
const uint32_t width = std::max(1.0f, std::floor(bloomWidth));
const uint32_t height = std::max(1.0f, std::floor(bloomHeight));
// we might need to adjust the max # of levels
const uint32_t major = 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, {
.width = desc.width / 2,
.height = desc.height / 2,
.format = outFormat
});
}
if constexpr(!isWebGL) {
struct BloomPassData {
FrameGraphId<FrameGraphTexture> in;
FrameGraphId<FrameGraphTexture> out;
};
// downsample phase
auto& bloomDownsamplePass = fg.addPass<BloomPassData>("Bloom Downsample",
[&](FrameGraph::Builder& builder, auto& data) {
data.in = builder.sample(input);
data.out = builder.createTexture("Bloom Texture", {
.width = width,
.height = height,
.levels = inoutBloomOptions.levels,
.format = outFormat
});
data.out = builder.sample(data.out);
for (size_t i = 0; i < inoutBloomOptions.levels; i++) {
auto out = builder.createSubresource(data.out, "Bloom Texture mip",
{ .level = uint8_t(i) });
builder.declareRenderPass(out);
}
},
[=](FrameGraphResources const& resources,
auto const& data, DriverApi& driver) {
auto const& material = getPostProcessMaterial("bloomDownsample");
FMaterialInstance* mi = material.getMaterialInstance();
const PipelineState pipeline(material.getPipelineState());
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);
mi->setParameter("threshold", inoutBloomOptions.threshold ? 1.0f : 0.0f);
mi->setParameter("invHighlight",
std::isinf(inoutBloomOptions.highlight) ? 0.0f : 1.0f
/ inoutBloomOptions.highlight);
for (size_t i = 0; i < inoutBloomOptions.levels; i++) {
auto hwOutRT = resources.getRenderPassInfo(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));
driver.setMinMaxLevels(hwOut, i,
i); // safe because we're using LINEAR_MIPMAP_NEAREST
}
driver.setMinMaxLevels(hwOut, 0, inoutBloomOptions.levels - 1);
});
input = bloomDownsamplePass->out;
// flare pass
auto& flarePass = fg.addPass<BloomPassData>("Flare",
[&](FrameGraph::Builder& builder, auto& data) {
data.in = builder.sample(input);
data.out = builder.createTexture("Flare Texture", {
.width = width / 2,
.height = height / 2,
.format = outFormat
});
data.out = builder.declareRenderPass(data.out);
},
[=](FrameGraphResources const& resources,
auto const& data, DriverApi& driver) {
auto in = resources.getTexture(data.in);
auto out = resources.getRenderPassInfo(0);
const float aspectRatio = float(width) / height;
auto const& material = getPostProcessMaterial("flare");
FMaterialInstance* mi = material.getMaterialInstance();
mi->setParameter("color", in, {
.filterMag = SamplerMagFilter::LINEAR,
.filterMin = SamplerMinFilter::LINEAR_MIPMAP_NEAREST
});
mi->setParameter("level", 1.0f); // adjust with resolution
mi->setParameter("aspectRatio",
float2{ aspectRatio, 1.0f / aspectRatio });
mi->setParameter("threshold",
float2{ inoutBloomOptions.ghostThreshold, inoutBloomOptions.haloThreshold });
mi->setParameter("chromaticAberration",
inoutBloomOptions.chromaticAberration);
mi->setParameter("ghostCount", (float)inoutBloomOptions.ghostCount);
mi->setParameter("ghostSpacing", inoutBloomOptions.ghostSpacing);
mi->setParameter("haloRadius", inoutBloomOptions.haloRadius);
mi->setParameter("haloThickness", inoutBloomOptions.haloThickness);
commitAndRender(out, material, driver);
});
auto flare = gaussianBlurPass(fg,
flarePass->out, 0,
{}, 0, 0,
false, 9);
fg.getBlackboard().put("flare", flare);
// upsample phase
auto& bloomUpsamplePass = fg.addPass<BloomPassData>("Bloom Upsample",
[&](FrameGraph::Builder& builder, auto& data) {
data.in = builder.sample(input);
data.out = input;
for (size_t i = 0; i < inoutBloomOptions.levels; i++) {
auto out = builder.createSubresource(data.out, "Bloom Texture mip",
{ .level = uint8_t(i) });
builder.declareRenderPass(out);
}
},
[=](FrameGraphResources const& resources,
auto const& data, DriverApi& driver) {
auto hwIn = resources.getTexture(data.in);
auto const& outDesc = resources.getDescriptor(data.out);
auto const& material = getPostProcessMaterial("bloomUpsample");
FMaterialInstance* mi = material.getMaterialInstance();
PipelineState pipeline(material.getPipelineState());
pipeline.rasterState.blendFunctionSrcRGB = BlendFunction::ONE;
pipeline.rasterState.blendFunctionDstRGB = BlendFunction::ONE;
mi->use(driver);
for (size_t i = inoutBloomOptions.levels - 1; i >= 1; i--) {
auto hwDstRT = resources.getRenderPassInfo(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", hwIn, {
.filterMag = SamplerMagFilter::LINEAR,
.filterMin = SamplerMinFilter::LINEAR_MIPMAP_NEAREST
});
mi->setParameter("level", float(i));
driver.setMinMaxLevels(hwIn, i, i);
mi->commit(driver);
driver.beginRenderPass(hwDstRT.target, hwDstRT.params);
driver.draw(pipeline, fullScreenRenderPrimitive);
driver.endRenderPass();
}
driver.setMinMaxLevels(hwIn, 0, inoutBloomOptions.levels - 1);
});
return bloomUpsamplePass->out;
} else { // !isWebGL
struct BloomPassData {
FrameGraphId<FrameGraphTexture> in;
FrameGraphId<FrameGraphTexture> out;
FrameGraphId<FrameGraphTexture> stage;
uint32_t outRT[kMaxBloomLevels];
uint32_t stageRT[kMaxBloomLevels];
};
// downsample phase
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,
.levels = inoutBloomOptions.levels,
.format = outFormat
});
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) });
builder.declareRenderPass(out, &data.outRT[i]);
builder.declareRenderPass(stage, &data.stageRT[i]);
}
},
[=](FrameGraphResources const& resources,
auto const& data, DriverApi& driver) {
auto const& material = getPostProcessMaterial("bloomDownsample");
FMaterialInstance* mi = material.getMaterialInstance();
const PipelineState pipeline(material.getPipelineState());
auto hwIn = resources.getTexture(data.in);
auto hwOut = resources.getTexture(data.out);
auto hwStage = resources.getTexture(data.stage);
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);
mi->setParameter("threshold", inoutBloomOptions.threshold ? 1.0f : 0.0f);
mi->setParameter("invHighlight",
std::isinf(inoutBloomOptions.highlight) ? 0.0f : 1.0f
/ inoutBloomOptions.highlight);
for (size_t i = 0; i < inoutBloomOptions.levels; i++) {
const bool parity = (i % 2) == 0;
auto hwDstRT = resources.getRenderPassInfo(
parity ? data.outRT[i] : data.stageRT[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);
hwDstRT.params.flags.discardStart = TargetBufferFlags::COLOR;
hwDstRT.params.flags.discardEnd = TargetBufferFlags::NONE;
driver.beginRenderPass(hwDstRT.target, hwDstRT.params);
driver.draw(pipeline, fullScreenRenderPrimitive);
driver.endRenderPass();
// prepare the next level
mi->setParameter("source", parity ? hwOut : hwStage, {
.filterMag = SamplerMagFilter::LINEAR,
.filterMin = SamplerMinFilter::LINEAR_MIPMAP_NEAREST
});
mi->setParameter("level", float(i));
}
});
FrameGraphId<FrameGraphTexture> output = bloomDownsamplePass->out;
FrameGraphId<FrameGraphTexture> stage = bloomDownsamplePass->stage;
// upsample phase
auto& bloomUpsamplePass = fg.addPass<BloomPassData>("Bloom Upsample",
[&](FrameGraph::Builder& builder, auto& data) {
data.out = builder.sample(output);
data.stage = builder.sample(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) });
builder.declareRenderPass(out, &data.outRT[i]);
builder.declareRenderPass(stage, &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");
FMaterialInstance* mi = material.getMaterialInstance();
PipelineState pipeline(material.getPipelineState());
pipeline.rasterState.blendFunctionSrcRGB = BlendFunction::ONE;
pipeline.rasterState.blendFunctionDstRGB = BlendFunction::ONE;
mi->use(driver);
for (size_t j = inoutBloomOptions.levels, i = j - 1; i >= 1; i--, j++) {
const bool parity = (j % 2) == 0;
auto hwDstRT = resources.getRenderPassInfo(
parity ? data.outRT[i - 1] : data.stageRT[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", parity ? hwStage : 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();
}
// 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);
}
});
return bloomUpsamplePass->out;
}
}
UTILS_NOINLINE
static float4 getVignetteParameters(VignetteOptions const& options,
uint32_t width, uint32_t height) noexcept {
if (options.enabled) {
// Vignette params
// From 0.0 to 0.5 the vignette is a rounded rect that turns into an oval
// From 0.5 to 1.0 the vignette turns from oval to circle
float oval = min(options.roundness, 0.5f) * 2.0f;
float circle = (max(options.roundness, 0.5f) - 0.5f) * 2.0f;
float roundness = (1.0f - oval) * 6.0f + oval;
// Mid point varies during the oval/rounded section of roundness
// We also modify it to emphasize feathering
float midPoint = (1.0f - options.midPoint) * mix(2.2f, 3.0f, oval)
* (1.0f - 0.1f * options.feather);
// Radius of the rounded corners as a param to pow()
float radius = roundness *
mix(1.0f + 4.0f * (1.0f - options.feather), 1.0f, std::sqrt(oval));
// Factor to transform oval into circle
float aspect = mix(1.0f, float(width) / float(height), circle);
return float4{ midPoint, radius, aspect, options.feather };
}
// Set half-max to show disabled
return float4{ std::numeric_limits<half>::max() };
}
void PostProcessManager::colorGradingPrepareSubpass(DriverApi& driver,
const FColorGrading* colorGrading, ColorGradingConfig const& colorGradingConfig,
VignetteOptions const& vignetteOptions, uint32_t width, uint32_t height) noexcept {
float4 vignetteParameters = getVignetteParameters(vignetteOptions, width, height);
auto const& material = getPostProcessMaterial("colorGradingAsSubpass");
FMaterialInstance* mi = material.getMaterialInstance();
mi->setParameter("lut", colorGrading->getHwHandle(), {
.filterMag = SamplerMagFilter::LINEAR,
.filterMin = SamplerMinFilter::LINEAR,
.wrapS = SamplerWrapMode::CLAMP_TO_EDGE,
.wrapT = SamplerWrapMode::CLAMP_TO_EDGE,
.wrapR = SamplerWrapMode::CLAMP_TO_EDGE,
.anisotropyLog2 = 0
});
const float lutDimension = float(colorGrading->getDimension());
mi->setParameter("lutSize", float2{
0.5f / lutDimension, (lutDimension - 1.0f) / lutDimension,
});
const float temporalNoise = mUniformDistribution(mEngine.getRandomEngine());
mi->setParameter("vignette", vignetteParameters);
mi->setParameter("vignetteColor", vignetteOptions.color);
mi->setParameter("dithering", colorGradingConfig.dithering);
mi->setParameter("fxaa", colorGradingConfig.fxaa);
mi->setParameter("temporalNoise", temporalNoise);
mi->commit(driver);
}
void PostProcessManager::colorGradingSubpass(DriverApi& driver,
ColorGradingConfig const& colorGradingConfig) noexcept {
FEngine& engine = mEngine;
Handle<HwRenderPrimitive> const& fullScreenRenderPrimitive = engine.getFullScreenRenderPrimitive();
auto const& material = getPostProcessMaterial("colorGradingAsSubpass");
// the UBO has been set and committed in colorGradingPrepareSubpass()
FMaterialInstance* mi = material.getMaterialInstance();
mi->use(driver);
const uint8_t variant = uint8_t(colorGradingConfig.translucent ?
PostProcessVariant::TRANSLUCENT : PostProcessVariant::OPAQUE);
driver.nextSubpass();
driver.draw(material.getPipelineState(variant), fullScreenRenderPrimitive);
}
void PostProcessManager::customResolvePrepareSubpass(DriverApi& driver, CustomResolveOp op) noexcept {
auto const& material = getPostProcessMaterial("customResolveAsSubpass");
FMaterialInstance* mi = material.getMaterialInstance();
mi->setParameter("direction", op == CustomResolveOp::COMPRESS ? 1.0f : -1.0f),
mi->commit(driver);
}
void PostProcessManager::customResolveSubpass(DriverApi& driver) noexcept {
FEngine& engine = mEngine;
Handle<HwRenderPrimitive> const& fullScreenRenderPrimitive = engine.getFullScreenRenderPrimitive();
auto const& material = getPostProcessMaterial("customResolveAsSubpass");
// the UBO has been set and committed in colorGradingPrepareSubpass()
FMaterialInstance* mi = material.getMaterialInstance();
mi->use(driver);
driver.nextSubpass();
driver.draw(material.getPipelineState(), fullScreenRenderPrimitive);
}
FrameGraphId<FrameGraphTexture> PostProcessManager::customResolveUncompressPass(FrameGraph& fg,
FrameGraphId<FrameGraphTexture> inout) noexcept {
struct UncompressData {
FrameGraphId<FrameGraphTexture> inout;
};
auto& detonemapPass = fg.addPass<UncompressData>("Uncompress Pass",
[&](FrameGraph::Builder& builder, auto& data) {
data.inout = builder.read(inout, FrameGraphTexture::Usage::SUBPASS_INPUT);
data.inout = builder.write(data.inout, FrameGraphTexture::Usage::COLOR_ATTACHMENT);
builder.declareRenderPass("Uncompress target", {
.attachments = { .color = { data.inout }}
});
},
[=](FrameGraphResources const& resources, auto const& data, DriverApi& driver) {
customResolvePrepareSubpass(driver, CustomResolveOp::UNCOMPRESS);
auto out = resources.getRenderPassInfo();
out.params.subpassMask = 1;
driver.beginRenderPass(out.target, out.params);
customResolveSubpass(driver);
driver.endRenderPass();
});
return detonemapPass->inout;
}
FrameGraphId<FrameGraphTexture> PostProcessManager::colorGrading(FrameGraph& fg,
FrameGraphId<FrameGraphTexture> input,
FColorGrading const* colorGrading, ColorGradingConfig const& colorGradingConfig,
BloomOptions const& bloomOptions, VignetteOptions const& vignetteOptions,
float2 scale) noexcept
{
Blackboard& blackboard = fg.getBlackboard();
FrameGraphId<FrameGraphTexture> bloomDirt;
FrameGraphId<FrameGraphTexture> starburst;
FrameGraphId<FrameGraphTexture> bloom = blackboard.get<FrameGraphTexture>("bloom");
FrameGraphId<FrameGraphTexture> flare = blackboard.get<FrameGraphTexture>("flare");
float bloomStrength = 0.0f;
if (bloomOptions.enabled) {
bloomStrength = clamp(bloomOptions.strength, 0.0f, 1.0f);
if (bloomOptions.dirt) {
FTexture* fdirt = upcast(bloomOptions.dirt);
FrameGraphTexture frameGraphTexture{ .handle = fdirt->getHwHandle() };
bloomDirt = fg.import("dirt", {
.width = (uint32_t)fdirt->getWidth(0u),
.height = (uint32_t)fdirt->getHeight(0u),
.format = fdirt->getFormat()
}, FrameGraphTexture::Usage::SAMPLEABLE, frameGraphTexture);
}
if (bloomOptions.lensFlare && bloomOptions.starburst) {
starburst = fg.import("starburst", {
.width = 256, .height = 1, .format = TextureFormat::R8
}, FrameGraphTexture::Usage::SAMPLEABLE,
FrameGraphTexture{ .handle = mStarburstTexture });
}
}
struct PostProcessColorGrading {
FrameGraphId<FrameGraphTexture> input;
FrameGraphId<FrameGraphTexture> output;
FrameGraphId<FrameGraphTexture> bloom;
FrameGraphId<FrameGraphTexture> flare;
FrameGraphId<FrameGraphTexture> dirt;
FrameGraphId<FrameGraphTexture> starburst;
};
auto& ppColorGrading = fg.addPass<PostProcessColorGrading>("colorGrading",
[&](FrameGraph::Builder& builder, auto& data) {
auto const& inputDesc = fg.getDescriptor(input);
data.input = builder.sample(input);
data.output = builder.createTexture("colorGrading output", {
.width = inputDesc.width,
.height = inputDesc.height,
.format = colorGradingConfig.ldrFormat
});
data.output = builder.declareRenderPass(data.output);
if (bloom) {
data.bloom = builder.sample(bloom);
}
if (bloomDirt) {
data.dirt = builder.sample(bloomDirt);
}
if (bloomOptions.lensFlare && flare) {
data.flare = builder.sample(flare);
if (starburst) {
data.starburst = builder.sample(starburst);
}
}
},
[=](FrameGraphResources const& resources, auto const& data, DriverApi& driver) {
Handle<HwTexture> colorTexture = resources.getTexture(data.input);
Handle<HwTexture> bloomTexture =
data.bloom ? resources.getTexture(data.bloom) : getZeroTexture();
Handle<HwTexture> flareTexture =
data.flare ? resources.getTexture(data.flare) : getZeroTexture();
Handle<HwTexture> dirtTexture =
data.dirt ? resources.getTexture(data.dirt) : getOneTexture();
Handle<HwTexture> starburstTexture =
data.starburst ? resources.getTexture(data.starburst) : getOneTexture();
auto const& out = resources.getRenderPassInfo();
auto const& material = getPostProcessMaterial("colorGrading");
FMaterialInstance* mi = material.getMaterialInstance();
mi->setParameter("lut", colorGrading->getHwHandle(), {
.filterMag = SamplerMagFilter::LINEAR,
.filterMin = SamplerMinFilter::LINEAR
});
const float lutDimension = float(colorGrading->getDimension());
mi->setParameter("lutSize", float2{
0.5f / lutDimension, (lutDimension - 1.0f) / lutDimension,
});
mi->setParameter("colorBuffer", colorTexture, { /* shader uses texelFetch */ });
mi->setParameter("bloomBuffer", bloomTexture, {
.filterMag = SamplerMagFilter::LINEAR,
.filterMin = SamplerMinFilter::LINEAR /* always read base level in shader */
});
mi->setParameter("flareBuffer", flareTexture, {
.filterMag = SamplerMagFilter::LINEAR,
.filterMin = SamplerMinFilter::LINEAR
});
mi->setParameter("dirtBuffer", dirtTexture, {
.filterMag = SamplerMagFilter::LINEAR,
.filterMin = SamplerMinFilter::LINEAR
});
mi->setParameter("starburstBuffer", starburstTexture, {
.filterMag = SamplerMagFilter::LINEAR,
.filterMin = SamplerMinFilter::LINEAR,
.wrapS = SamplerWrapMode::REPEAT,
.wrapT = SamplerWrapMode::REPEAT
});
// Bloom params
float4 bloomParameters{
bloomStrength / float(bloomOptions.levels),
1.0f,
(bloomOptions.enabled && bloomOptions.dirt) ? bloomOptions.dirtStrength : 0.0f,
bloomOptions.lensFlare ? bloomStrength : 0.0f
};
if (bloomOptions.blendMode == BloomOptions::BlendMode::INTERPOLATE) {
bloomParameters.y = 1.0f - bloomParameters.x;
}
auto const& output = resources.getDescriptor(data.output);
float4 vignetteParameters = getVignetteParameters(
vignetteOptions, output.width, output.height);
const float temporalNoise = mUniformDistribution(mEngine.getRandomEngine());
mi->setParameter("dithering", colorGradingConfig.dithering);
mi->setParameter("bloom", bloomParameters);
mi->setParameter("vignette", vignetteParameters);
mi->setParameter("vignetteColor", vignetteOptions.color);
mi->setParameter("fxaa", colorGradingConfig.fxaa);
mi->setParameter("temporalNoise", temporalNoise);
const uint8_t variant = uint8_t(colorGradingConfig.translucent ?
PostProcessVariant::TRANSLUCENT : PostProcessVariant::OPAQUE);
commitAndRender(out, material, variant, driver);
}
);
return ppColorGrading->output;
}
FrameGraphId<FrameGraphTexture> PostProcessManager::fxaa(FrameGraph& fg,
FrameGraphId<FrameGraphTexture> input,
TextureFormat outFormat, bool translucent) noexcept {
struct PostProcessFXAA {
FrameGraphId<FrameGraphTexture> input;
FrameGraphId<FrameGraphTexture> output;
};
auto& ppFXAA = fg.addPass<PostProcessFXAA>("fxaa",
[&](FrameGraph::Builder& builder, auto& data) {
auto const& inputDesc = fg.getDescriptor(input);
data.input = builder.sample(input);
data.output = builder.createTexture("fxaa output", {
.width = inputDesc.width,
.height = inputDesc.height,
.format = outFormat
});
data.output = builder.declareRenderPass(data.output);
},
[=](FrameGraphResources const& resources,
auto const& data, DriverApi& driver) {
auto const& texture = resources.getTexture(data.input);
auto const& out = resources.getRenderPassInfo();
auto const& material = getPostProcessMaterial("fxaa");
FMaterialInstance* mi = material.getMaterialInstance();
mi->setParameter("colorBuffer", texture, {
.filterMag = SamplerMagFilter::LINEAR,
.filterMin = SamplerMinFilter::LINEAR
});
const uint8_t variant = uint8_t(translucent ?
PostProcessVariant::TRANSLUCENT : PostProcessVariant::OPAQUE);
commitAndRender(out, material, variant, driver);
});
return ppFXAA->output;
}
void PostProcessManager::prepareTaa(FrameHistory& frameHistory,
CameraInfo const& cameraInfo,
TemporalAntiAliasingOptions const& taaOptions) const noexcept {
auto const& previous = frameHistory[0];
auto& current = frameHistory.getCurrent();
// get sample position within a pixel [-0.5, 0.5]
const float2 jitter = halton(previous.frameId) - 0.5f;
// save this frame's sample position
current.jitter = jitter;
}
FrameGraphId<FrameGraphTexture> PostProcessManager::taa(FrameGraph& fg,
FrameGraphId<FrameGraphTexture> input, FrameHistory& frameHistory,
FrameGraphId<FrameGraphTexture> colorHistory,
TemporalAntiAliasingOptions const& taaOptions,
ColorGradingConfig colorGradingConfig) noexcept {
FrameHistoryEntry const& entry = frameHistory[0];
mat4f const* historyProjection = nullptr;
if (UTILS_UNLIKELY(!colorHistory)) {
// if we don't have a history yet, just use the current color buffer as history
colorHistory = input;
historyProjection = &frameHistory.getCurrent().projection;
} else {
historyProjection = &entry.projection;
}
Blackboard& blackboard = fg.getBlackboard();
auto depth = blackboard.get<FrameGraphTexture>("depth");
assert_invariant(depth);
struct TAAData {
FrameGraphId<FrameGraphTexture> color;
FrameGraphId<FrameGraphTexture> depth;
FrameGraphId<FrameGraphTexture> history;
FrameGraphId<FrameGraphTexture> output;
FrameGraphId<FrameGraphTexture> tonemappedOutput;
};
auto& taa = fg.addPass<TAAData>("TAA",
[&](FrameGraph::Builder& builder, auto& data) {
auto desc = fg.getDescriptor(input);
data.color = builder.sample(input);
data.depth = builder.sample(depth);
data.history = builder.sample(colorHistory);
data.output = builder.createTexture("TAA output", desc);
data.output = builder.write(data.output);
if (colorGradingConfig.asSubpass) {
data.tonemappedOutput = builder.createTexture("Tonemapped Buffer", {
.width = desc.width,
.height = desc.height,
.format = colorGradingConfig.ldrFormat
});
data.tonemappedOutput = builder.write(data.tonemappedOutput, FrameGraphTexture::Usage::COLOR_ATTACHMENT);
data.output = builder.read(data.output, FrameGraphTexture::Usage::SUBPASS_INPUT);
}
data.output = builder.write(data.output, FrameGraphTexture::Usage::COLOR_ATTACHMENT);
builder.declareRenderPass("TAA target", {
.attachments = { .color = { data.output, data.tonemappedOutput }}
});
},
[=, &frameHistory](FrameGraphResources const& resources, auto const& data, DriverApi& driver) {
constexpr mat4f normalizedToClip{mat4f::row_major_init{
2, 0, 0, -1,
0, 2, 0, -1,
0, 0, 1, 0,
0, 0, 0, 1
}};
constexpr float2 sampleOffsets[9] = {
{ -1.0f, -1.0f }, { 0.0f, -1.0f }, { 1.0f, -1.0f },
{ -1.0f, 0.0f }, { 0.0f, 0.0f }, { 1.0f, 0.0f },
{ -1.0f, 1.0f }, { 0.0f, 1.0f }, { 1.0f, 1.0f },
};
FrameHistoryEntry& current = frameHistory.getCurrent();
float sum = 0.0;
float weights[9];
// this doesn't get vectorized (probably because of exp()), so don't bother
// unrolling it.
#pragma nounroll
for (size_t i = 0; i < 9; i++) {
float2 d = sampleOffsets[i] - current.jitter;
d *= 1.0f / taaOptions.filterWidth;
// this is a gaussian fit of a 3.3 Blackman Harris window
// see: "High Quality Temporal Supersampling" by Bruan Karis
weights[i] = std::exp2(-3.3f * (d.x * d.x + d.y * d.y));
sum += weights[i];
}
for (auto& w : weights) {
w /= sum;
}
auto out = resources.getRenderPassInfo();
auto color = resources.getTexture(data.color);
auto depth = resources.getTexture(data.depth);
auto history = resources.getTexture(data.history);
auto const& material = getPostProcessMaterial("taa");
FMaterialInstance* mi = material.getMaterialInstance();
mi->setParameter("color", color, {}); // nearest
mi->setParameter("depth", depth, {}); // nearest
mi->setParameter("alpha", taaOptions.feedback);
mi->setParameter("history", history, {
.filterMag = SamplerMagFilter::LINEAR,
.filterMin = SamplerMinFilter::LINEAR
});
mi->setParameter("filterWeights", weights, 9);
mi->setParameter("reprojection",
*historyProjection *
inverse(current.projection) *
normalizedToClip);
mi->commit(driver);
mi->use(driver);
const uint8_t variant = uint8_t(colorGradingConfig.translucent ?
PostProcessVariant::TRANSLUCENT : PostProcessVariant::OPAQUE);
if (colorGradingConfig.asSubpass) {
out.params.subpassMask = 1;
}
driver.beginRenderPass(out.target, out.params);
driver.draw(material.getPipelineState(variant), mEngine.getFullScreenRenderPrimitive());
if (colorGradingConfig.asSubpass) {
colorGradingSubpass(driver, colorGradingConfig);
}
driver.endRenderPass();
});
return colorGradingConfig.asSubpass ? taa->tonemappedOutput : taa->output;
}
FrameGraphId<FrameGraphTexture> PostProcessManager::opaqueBlit(FrameGraph& fg,
FrameGraphId<FrameGraphTexture> input, FrameGraphTexture::Descriptor const& outDesc,
SamplerMagFilter filter) noexcept {
struct PostProcessScaling {
FrameGraphId<FrameGraphTexture> input;
FrameGraphId<FrameGraphTexture> output;
};
auto& ppBlit = fg.addPass<PostProcessScaling>("opaque blit",
[&](FrameGraph::Builder& builder, auto& data) {
// we currently have no use for this case, so we just assert. This is better for now to trap
// cases that we might not intend.
assert_invariant(fg.getDescriptor(input).samples <= 1);
data.output = builder.declareRenderPass(
builder.createTexture("opaque blit output", outDesc));
data.input = builder.read(input);
// We use a RenderPass for the source here, instead of just creating a render
// target from data.input in the execute closure, because data.input may refer to
// an imported render target and in this case data.input won't resolve to an actual
// HwTexture handle. Using a RenderPass works because data.input will resolve
// to the actual imported render target and will have the correct viewport.
builder.declareRenderPass("opaque blit input", {
.attachments = { .color = { data.input }}});
},
[=](FrameGraphResources const& resources, auto const& data, DriverApi& driver) {
auto out = resources.getRenderPassInfo(0);
auto in = resources.getRenderPassInfo(1);
driver.blit(TargetBufferFlags::COLOR,
out.target, out.params.viewport,
in.target, in.params.viewport,
filter);
});
// we rely on automatic culling of unused render passes
return ppBlit->output;
}
FrameGraphId<FrameGraphTexture> PostProcessManager::blendBlit(
FrameGraph& fg, bool translucent, DynamicResolutionOptions dsrOptions,
FrameGraphId<FrameGraphTexture> input,
FrameGraphTexture::Descriptor const& outDesc) noexcept {
Handle<HwRenderPrimitive> fullScreenRenderPrimitive = mEngine.getFullScreenRenderPrimitive();
bool lowQualityFallback = false;
if (translucent && dsrOptions.quality != QualityLevel::LOW) {
// FidelityFX-FSR doesn't support the alpha channel currently
dsrOptions.quality = QualityLevel::LOW;
lowQualityFallback = true;
}
const bool twoPassesEASU = mWorkaroundSplitEasu &&
(dsrOptions.quality == QualityLevel::MEDIUM
|| dsrOptions.quality == QualityLevel::HIGH);
// helper to set the EASU uniforms
auto setEasuUniforms = [](FMaterialInstance* mi,
FrameGraphTexture::Descriptor const& inputDesc,
FrameGraphTexture::Descriptor const& outputDesc) {
FSRUniforms uniforms{};
FSR_ScalingSetup(&uniforms, {
.viewportWidth = inputDesc.width,
.viewportHeight = inputDesc.height,
.inputWidth = inputDesc.width,
.inputHeight = inputDesc.height,
.outputWidth = outputDesc.width,
.outputHeight = outputDesc.height,
});
mi->setParameter("EasuCon0", uniforms.EasuCon0);
mi->setParameter("EasuCon1", uniforms.EasuCon1);
mi->setParameter("EasuCon2", uniforms.EasuCon2);
mi->setParameter("EasuCon3", uniforms.EasuCon3);
mi->setParameter("textureSize",
float2{ inputDesc.width, inputDesc.height });
};
// helper to enable blending
auto enableTranslucentBlending = [](PipelineState& pipeline) {
pipeline.rasterState.blendFunctionSrcRGB = BlendFunction::ONE;
pipeline.rasterState.blendFunctionSrcAlpha = BlendFunction::ONE;
pipeline.rasterState.blendFunctionDstRGB = BlendFunction::ONE_MINUS_SRC_ALPHA;
pipeline.rasterState.blendFunctionDstAlpha = BlendFunction::ONE_MINUS_SRC_ALPHA;
};
struct QuadBlitData {
FrameGraphId<FrameGraphTexture> input;
FrameGraphId<FrameGraphTexture> output;
FrameGraphId<FrameGraphTexture> depth;
};
auto& ppQuadBlit = fg.addPass<QuadBlitData>(dsrOptions.enabled ? "quad scaling" : "blendBlit",
[&](FrameGraph::Builder& builder, auto& data) {
data.input = builder.sample(input);
data.output = builder.createTexture("scaled output", outDesc);
if (twoPassesEASU) {
// FIXME: it would be better to use the stencil buffer in this case
data.depth = builder.createTexture("scaled output depth", {
.width = outDesc.width,
.height = outDesc.height,
.format = TextureFormat::DEPTH16
});
data.depth = builder.write(data.depth, FrameGraphTexture::Usage::DEPTH_ATTACHMENT);
}
data.output = builder.write(data.output, FrameGraphTexture::Usage::COLOR_ATTACHMENT);
builder.declareRenderPass(builder.getName(data.output), {
.attachments = { .color = { data.output }, .depth = { data.depth }},
.clearFlags = TargetBufferFlags::DEPTH });
},
[=](FrameGraphResources const& resources,
auto const& data, DriverApi& driver) {
auto color = resources.getTexture(data.input);
auto const& inputDesc = resources.getDescriptor(data.input);
auto const& outputDesc = resources.getDescriptor(data.output);
// --------------------------------------------------------------------------------
// set uniforms
PostProcessMaterial* splitEasuMaterial = nullptr;
PostProcessMaterial* easuMaterial = nullptr;
if (twoPassesEASU) {
splitEasuMaterial = &getPostProcessMaterial("fsr_easu_mobileF");
auto* mi = splitEasuMaterial->getMaterialInstance();
setEasuUniforms(mi, inputDesc, outputDesc);
mi->setParameter("color", color, {
.filterMag = SamplerMagFilter::LINEAR,
.filterMin = SamplerMinFilter::LINEAR
});
mi->setParameter("resolution",
float4{ outputDesc.width, outputDesc.height,
1.0f / outputDesc.width, 1.0f / outputDesc.height });
mi->commit(driver);
}
{ // just a scope to not leak local variables
const StaticString blitterNames[4] = {
"blitLow", "fsr_easu_mobile", "fsr_easu_mobile", "fsr_easu" };
unsigned index = std::min(3u, (unsigned)dsrOptions.quality);
easuMaterial = &getPostProcessMaterial(blitterNames[index]);
auto* mi = easuMaterial->getMaterialInstance();
if (dsrOptions.quality != QualityLevel::LOW) {
setEasuUniforms(mi, inputDesc, outputDesc);
}
mi->setParameter("color", color, {
.filterMag = SamplerMagFilter::LINEAR,
.filterMin = SamplerMinFilter::LINEAR
});
mi->setParameter("resolution",
float4{ outputDesc.width, outputDesc.height,
1.0f / outputDesc.width, 1.0f / outputDesc.height });
mi->commit(driver);
}
// --------------------------------------------------------------------------------
// render pass with draw calls
auto out = resources.getRenderPassInfo();
driver.beginRenderPass(out.target, out.params);
if (twoPassesEASU) {
auto* mi = splitEasuMaterial->getMaterialInstance();
mi->use(driver);
PipelineState pipeline(splitEasuMaterial->getPipelineState());
if (translucent) {
enableTranslucentBlending(pipeline);
}
driver.draw(pipeline, fullScreenRenderPrimitive);
}
{ // scope to not leak local variables
auto* mi = easuMaterial->getMaterialInstance();
mi->use(driver);
PipelineState pipeline(easuMaterial->getPipelineState());
if (translucent) {
enableTranslucentBlending(pipeline);
}
if (twoPassesEASU) {
pipeline.rasterState.depthFunc = backend::SamplerCompareFunc::NE;
}
driver.draw(pipeline, fullScreenRenderPrimitive);
}
driver.endRenderPass();
});
auto output = ppQuadBlit->output;
// if we had to take the low quality fallback, we still do the "sharpen pass"
if (dsrOptions.sharpness > 0.0f && (dsrOptions.quality != QualityLevel::LOW || lowQualityFallback)) {
auto& ppFsrRcas = fg.addPass<QuadBlitData>("FidelityFX FSR1 Rcas",
[&](FrameGraph::Builder& builder, auto& data) {
data.input = builder.sample(output);
data.output = builder.createTexture("FFX FSR1 Rcas output", outDesc);
data.output = builder.declareRenderPass(data.output);
},
[=](FrameGraphResources const& resources,
auto const& data, DriverApi& driver) {
auto color = resources.getTexture(data.input);
auto out = resources.getRenderPassInfo();
auto const& outputDesc = resources.getDescriptor(data.output);
auto& material = getPostProcessMaterial("fsr_rcas");
FMaterialInstance* const mi = material.getMaterialInstance();
FSRUniforms uniforms;
FSR_SharpeningSetup(&uniforms, { .sharpness = 2.0f - 2.0f * dsrOptions.sharpness });
mi->setParameter("RcasCon", uniforms.RcasCon);
mi->setParameter("color", color, { }); // uses texelFetch
mi->setParameter("resolution", float4{
outputDesc.width, outputDesc.height,
1.0f / outputDesc.width, 1.0f / outputDesc.height });
mi->commit(driver);
mi->use(driver);
const uint8_t variant = uint8_t(translucent ?
PostProcessVariant::TRANSLUCENT : PostProcessVariant::OPAQUE);
PipelineState pipeline(material.getPipelineState(variant));
driver.beginRenderPass(out.target, out.params);
driver.draw(pipeline, fullScreenRenderPrimitive);
driver.endRenderPass();
});
output = ppFsrRcas->output;
}
// we rely on automatic culling of unused render passes
return output;
}
FrameGraphId<FrameGraphTexture> PostProcessManager::resolve(FrameGraph& fg,
const char* outputBufferName, FrameGraphId<FrameGraphTexture> input) noexcept {
// Don't do anything if we're not a MSAA buffer
auto desc = fg.getDescriptor(input);
if (desc.samples <= 1) {
return input;
}
struct ResolveData {
FrameGraphId<FrameGraphTexture> input;
FrameGraphId<FrameGraphTexture> output;
backend::TargetBufferFlags inFlags;
FrameGraphTexture::Usage usage;
};
auto& ppResolve = fg.addPass<ResolveData>("resolve",
[&](FrameGraph::Builder& builder, auto& data) {
FrameGraphRenderPass::Descriptor rpDesc;
auto& rpDescAttachment = isDepthFormat(desc.format) ?
rpDesc.attachments.depth :
rpDesc.attachments.color[0];
data.usage = isDepthFormat(desc.format) ?
FrameGraphTexture::Usage::DEPTH_ATTACHMENT :
FrameGraphTexture::Usage::COLOR_ATTACHMENT;
data.inFlags = isDepthFormat(desc.format) ?
backend::TargetBufferFlags::DEPTH :
backend::TargetBufferFlags::COLOR0;
data.input = builder.read(input, data.usage);
auto outputDesc = desc;
outputDesc.levels = 1;
outputDesc.samples = 0;
rpDescAttachment = builder.createTexture(outputBufferName, outputDesc);
rpDescAttachment = builder.write(rpDescAttachment, data.usage);
data.output = rpDescAttachment;
builder.declareRenderPass("Resolve Pass", rpDesc);
},
[](FrameGraphResources const& resources, auto const& data, DriverApi& driver) {
auto inDesc = resources.getDescriptor(data.input);
auto in = resources.getTexture(data.input);
auto out = resources.getRenderPassInfo();
assert_invariant(in);
Handle<HwRenderTarget> inRt;
if (data.usage == FrameGraphTexture::Usage::COLOR_ATTACHMENT) {
inRt = driver.createRenderTarget(data.inFlags,
out.params.viewport.width, out.params.viewport.height,
inDesc.samples, { in }, {}, {});
}
if (data.usage == FrameGraphTexture::Usage::DEPTH_ATTACHMENT) {
inRt = driver.createRenderTarget(data.inFlags,
out.params.viewport.width, out.params.viewport.height,
inDesc.samples, {}, { in }, {});
}
driver.blit(data.inFlags,
out.target, out.params.viewport, inRt, out.params.viewport,
SamplerMagFilter::NEAREST);
driver.destroyRenderTarget(inRt);
});
return ppResolve->output;
}
FrameGraphId<FrameGraphTexture> PostProcessManager::vsmMipmapPass(FrameGraph& fg,
FrameGraphId<FrameGraphTexture> input, uint8_t layer, size_t level,
math::float4 clearColor, bool finalize) noexcept {
struct VsmMipData {
FrameGraphId<FrameGraphTexture> in;
};
auto& depthMipmapPass = fg.addPass<VsmMipData>("VSM Generate Mipmap Pass",
[&](FrameGraph::Builder& builder, auto& data) {
const char* name = builder.getName(input);
data.in = builder.sample(input);
auto out = builder.createSubresource(data.in, "Mip level", {
.level = uint8_t(level + 1), .layer = layer });
out = builder.write(out, FrameGraphTexture::Usage::COLOR_ATTACHMENT);
builder.declareRenderPass(name, {
.attachments = { .color = { out }},
.clearColor = clearColor,
.clearFlags = TargetBufferFlags::COLOR
});
},
[=](FrameGraphResources const& resources,
auto const& data, DriverApi& driver) {
auto in = resources.getTexture(data.in);
auto out = resources.getRenderPassInfo();
auto const& inDesc = resources.getDescriptor(data.in);
auto width = inDesc.width;
assert_invariant(width == inDesc.height);
int dim = width >> (level + 1);
driver.setMinMaxLevels(in, level, level);
auto& material = getPostProcessMaterial("vsmMipmap");
// When generating shadow map mip levels, we want to preserve the 1 texel border.
// (note clearing never respects the scissor in filament)
PipelineState pipeline(material.getPipelineState());
pipeline.scissor = { 1u, 1u, dim - 2u, dim - 2u };
FMaterialInstance* const mi = material.getMaterialInstance();
mi->setParameter("color", in, {
.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 / dim);
mi->commit(driver);
mi->use(driver);
driver.beginRenderPass(out.target, out.params);
driver.draw(pipeline, mEngine.getFullScreenRenderPrimitive());
driver.endRenderPass();
if (finalize) {
driver.setMinMaxLevels(in, 0, level);
}
});
return depthMipmapPass->in;
}
} // namespace filament