Generate the kernel texture on the GPU

This avoids having to upload data from the cpu.
This commit is contained in:
Mathias Agopian
2021-04-20 13:23:29 -07:00
committed by Mathias Agopian
parent e372cc25f2
commit 0427787cb7
4 changed files with 167 additions and 51 deletions

View File

@@ -22,6 +22,7 @@ set(PRIVATE_HDRS
)
set(MATERIAL_SRCS
src/materials/generateKernel.mat
src/materials/iblprefilter.mat
)

View File

@@ -164,6 +164,7 @@ public:
friend class Instance;
filament::Texture* createReflectionsTexture();
IBLPrefilterContext& mContext;
filament::Material* mKernelMaterial = nullptr;
filament::Texture* mKernelTexture = nullptr;
float* mKernelWeightArray = nullptr;
uint32_t mSampleCount = 0u;
@@ -181,8 +182,7 @@ private:
utils::Entity mFullScreenQuadEntity{};
utils::Entity mCameraEntity{};
filament::View* mView{};
// this one might have to move into Filter
filament::Material* mMaterial{};
filament::Material* mIntegrationMaterial{};
};
#endif //TNT_IBL_PREFILTER_IBLPREFILTER_H

View File

@@ -101,7 +101,7 @@ IBLPrefilterContext::IBLPrefilterContext(Engine& engine)
mCameraEntity = em.create();
mFullScreenQuadEntity = em.create();
mMaterial = Material::Builder().package(
mIntegrationMaterial = Material::Builder().package(
IBLPREFILTER_MATERIALS_IBLPREFILTER_DATA,
IBLPREFILTER_MATERIALS_IBLPREFILTER_SIZE).build(engine);
@@ -127,7 +127,7 @@ IBLPrefilterContext::IBLPrefilterContext(Engine& engine)
RenderableManager::Builder(1)
.geometry(0, RenderableManager::PrimitiveType::TRIANGLES, mVertexBuffer, mIndexBuffer)
.material(0, mMaterial->getDefaultInstance())
.material(0, mIntegrationMaterial->getDefaultInstance())
.culling(false)
.castShadows(false)
.receiveShadows(false)
@@ -157,7 +157,7 @@ IBLPrefilterContext::~IBLPrefilterContext() noexcept {
engine.destroy(mRenderer);
engine.destroy(mVertexBuffer);
engine.destroy(mIndexBuffer);
engine.destroy(mMaterial);
engine.destroy(mIntegrationMaterial);
engine.destroy(mFullScreenQuadEntity);
engine.destroyCameraComponent(mCameraEntity);
em.destroy(mFullScreenQuadEntity);
@@ -179,7 +179,7 @@ IBLPrefilterContext& IBLPrefilterContext::operator=(IBLPrefilterContext&& rhs) {
swap(mCamera, rhs.mCamera);
swap(mFullScreenQuadEntity, rhs.mFullScreenQuadEntity);
swap(mCameraEntity, rhs.mCameraEntity);
swap(mMaterial, rhs.mMaterial);
swap(mIntegrationMaterial, rhs.mIntegrationMaterial);
swap(mView, rhs.mView);
}
return *this;
@@ -193,65 +193,69 @@ IBLPrefilterContext::SpecularFilter::SpecularFilter(IBLPrefilterContext& context
using namespace backend;
Engine& engine = mContext.mEngine;
View* const view = mContext.mView;
Renderer* const renderer = mContext.mRenderer;
mSampleCount = std::min(config.sampleCount, uint16_t(2048));
mLevelCount = std::max(config.levelCount, uint8_t(1u));
mKernelMaterial = Material::Builder().package(
IBLPREFILTER_MATERIALS_GENERATEKERNEL_DATA,
IBLPREFILTER_MATERIALS_GENERATEKERNEL_SIZE).build(engine);
// { L.x, L.y, L.z, lod }
mKernelTexture = Texture::Builder()
.sampler(Texture::Sampler::SAMPLER_2D)
.format(Texture::InternalFormat::RGBA16F)
.usage(Texture::Usage::UPLOADABLE | Texture::Usage::SAMPLEABLE)
.usage(Texture::Usage::SAMPLEABLE | Texture::Usage::COLOR_ATTACHMENT)
.width(mLevelCount)
.height(mSampleCount)
.build(engine);
MaterialInstance* const mi = mKernelMaterial->getDefaultInstance();
mi->setParameter("size", uint2{ mLevelCount, mSampleCount });
mi->setParameter("sampleBits", uint32_t(std::log2(mSampleCount) + 0.5f));
mi->setParameter("sampleCount", float(mSampleCount));
mi->setParameter("oneOverLevelsMinusOne", 1.0f / (mLevelCount - 1.0f));
RenderableManager& rcm = engine.getRenderableManager();
rcm.setMaterialInstanceAt(
rcm.getInstance(mContext.mFullScreenQuadEntity), 0, mi);
RenderTarget* const rt = RenderTarget::Builder()
.texture(RenderTarget::AttachmentPoint::COLOR0, mKernelTexture)
.build(engine);
view->setRenderTarget(rt);
view->setViewport({ 0, 0, mLevelCount, mSampleCount });
renderer->renderStandaloneView(view);
engine.destroy(rt);
// the code below must match the shader in generateKernel.mat
// this is a little bit unfortunate that we have to compute the weightSum here, but it's
// not too heavy.
const uint32_t levelCount = mLevelCount;
const float sampleCount = mSampleCount;
mKernelWeightArray = new float[mLevelCount];
const uint32_t sampleCount = mSampleCount;
const size_t levels = mLevelCount;
// TODO: do this with a shader
for (uint32_t lod = 0 ; lod < levels; lod++) {
for (uint32_t lod = 0 ; lod < levelCount; lod++) {
SYSTRACE_NAME("computeFilterLOD");
const float perceptualRoughness = lodToPerceptualRoughness(saturate(lod / (levels - 1.0f)));
const float perceptualRoughness = lodToPerceptualRoughness(saturate(lod / (levelCount - 1.0f)));
const float roughness = perceptualRoughness * perceptualRoughness;
uint32_t effectiveSampleCount = sampleCount;
if (lod == 0) {
effectiveSampleCount = 1;
}
// compute the coefficients
half4* const p = (half4*)engine.streamAlloc(effectiveSampleCount * sizeof(half4));
const uint32_t effectiveSampleCount = (lod == 0) ? 1u : sampleCount;
float weight = 0.0f;
for (size_t i = 0; i < effectiveSampleCount; i++) {
const float2 u = hammersley(uint32_t(i), 1.0f / float(effectiveSampleCount));
const float3 H = hemisphereImportanceSampleDggx(u, roughness);
const float NoH = H.z;
const float NoH2 = H.z * H.z;
const float NoL = saturate(2 * NoH2 - 1);
const float3 L(2 * NoH * H.x, 2 * NoH * H.y, NoL);
const float pdf = DistributionGGX(NoH, roughness) / 4;
const float invOmegaS = effectiveSampleCount * pdf;
const float l = 1.0f - log4(invOmegaS);
weight += NoL; // note: L.z == NoL
p[i] = { L, l };
weight += NoL;
}
utils::slog.d << +lod << ": " << weight << utils::io::endl;
assert_invariant(lod < mLevelCount);
mKernelWeightArray[lod] = weight;
// upload the coefficients
mKernelTexture->setImage(engine, 0,
lod, 0, 1, effectiveSampleCount, {
p, effectiveSampleCount * sizeof(half4),
PixelDataFormat::RGBA, PixelDataType::HALF});
}
}
@@ -263,6 +267,7 @@ IBLPrefilterContext::SpecularFilter::SpecularFilter(IBLPrefilterContext& context
IBLPrefilterContext::SpecularFilter::~SpecularFilter() noexcept {
Engine& engine = mContext.mEngine;
engine.destroy(mKernelTexture);
engine.destroy(mKernelMaterial);
delete [] mKernelWeightArray;
}
@@ -336,14 +341,12 @@ filament::Texture* IBLPrefilterContext::SpecularFilter::operator()(
Engine& engine = mContext.mEngine;
View* const view = mContext.mView;
Renderer* const renderer = mContext.mRenderer;
Texture* const kernel = mKernelTexture;
MaterialInstance* const mi = mContext.mMaterial->getDefaultInstance();
MaterialInstance* const mi = mContext.mIntegrationMaterial->getDefaultInstance();
RenderableManager& rcm = engine.getRenderableManager();
rcm.setMaterialInstanceAt(
rcm.getInstance(mContext.mFullScreenQuadEntity), 0, mi);
const uint8_t inputLevels = environmentCubemap->getLevels();
const uint32_t sampleCount = mSampleCount;
const float linear = options.hdrLinear;
const float compress = options.hdrMax;
@@ -355,9 +358,9 @@ filament::Texture* IBLPrefilterContext::SpecularFilter::operator()(
environmentSampler.setMinFilter(SamplerMinFilter::LINEAR_MIPMAP_LINEAR);
mi->setParameter("environment", environmentCubemap, environmentSampler);
mi->setParameter("kernel", mKernelTexture, TextureSampler{ SamplerMagFilter::NEAREST });
mi->setParameter("compress", float2{ linear, compress });
if (options.generateMipmap) {
// We need mipmaps for prefiltering
environmentCubemap->generateMipmaps(engine);
@@ -374,7 +377,6 @@ filament::Texture* IBLPrefilterContext::SpecularFilter::operator()(
const float omegaP = (4.0f * f::PI) / float(6 * dim * dim);
mi->setParameter("kernel", kernel, TextureSampler{ SamplerMagFilter::NEAREST });
mi->setParameter("sampleCount", uint32_t(lod == 0 ? 1u : sampleCount));
mi->setParameter("attachmentLevel", uint32_t(lod));
mi->setParameter("invKernelWeight", 1.0f / mKernelWeightArray[lod]);
@@ -402,10 +404,5 @@ filament::Texture* IBLPrefilterContext::SpecularFilter::operator()(
dim >>= 1;
}
// {
// SYSTRACE_NAME("flushAndWait");
// engine.flushAndWait();
// }
return outReflectionsTexture;
}

View File

@@ -0,0 +1,118 @@
material {
name : generateKernel,
parameters : [
{
type : uint2,
name : size,
precision: high
},
{
type : uint,
name : sampleBits,
},
{
type : float,
name : sampleCount,
},
{
type : float,
name : oneOverLevelsMinusOne,
}
],
outputs : [
{
name : weight,
target : color,
type : float4
}
],
variables : [
vertex
],
domain : postprocess,
depthWrite : false,
depthCulling : false
}
vertex {
void postProcessVertex(inout PostProcessVertexInputs postProcess) {
postProcess.vertex.xy = postProcess.normalizedUV * vec2(materialParams.size);
}
}
fragment {
void dummy() {}
precision highp float;
float log4(const float x) {
return log2(x) * 0.5;
}
vec2 hammersley(const uint index, const float sampleCount, const uint sampleBits) {
// Compute Hammersley sequence
float invNumSamples = 1.0 / sampleCount;
uint i = index;
uint t = i;
uint bits = 0u;
for (uint j = 0u; j < sampleBits; j++) {
bits = bits * 2u + (t - (2u * (t / 2u)));
t /= 2u;
}
return vec2(float(i), float(bits)) * invNumSamples;
}
float DistributionGGX(const float NoH, const float a) {
// NOTE: (aa-1) == (a-1)(a+1) produces better fp accuracy
float f = (a - 1.0) * ((a + 1.0) * (NoH * NoH)) + 1.0;
return (a * a) / (PI * f * f);
}
vec3 hemisphereImportanceSampleDggx(const vec2 u, const float a) { // pdf = D(a) * cosTheta
float phi = 2.0 * PI * u.x;
// NOTE: (aa-1) == (a-1)(a+1) produces better fp accuracy
float cosTheta2 = (1.0 - u.y) / (1.0 + (a + 1.0) * ((a - 1.0) * u.y));
float cosTheta = sqrt(cosTheta2);
float sinTheta = sqrt(1.0 - cosTheta2);
return vec3(sinTheta * cos(phi), sinTheta * sin(phi), cosTheta);
}
vec4 computeWeight(const uint index, const float roughness) {
float sampleCount = materialParams.sampleCount;
vec2 u = hammersley(index, sampleCount, materialParams.sampleBits);
vec3 H = hemisphereImportanceSampleDggx(u, roughness);
float NoH = H.z;
float NoH2 = H.z * H.z;
float NoL = saturate(2.0 * NoH2 - 1.0);
vec3 L = vec3(2.0 * NoH * H.x, 2.0 * NoH * H.y, NoL);
float pdf = DistributionGGX(NoH, roughness) * 0.25;
float invOmegaS = sampleCount * pdf;
float l = 1.0f - log4(invOmegaS);
return vec4(L, l);
}
float lodToPerceptualRoughness(const float lod) {
// Inverse perceptualRoughness-to-LOD mapping:
// The LOD-to-perceptualRoughness mapping is a quadratic fit for
// log2(perceptualRoughness)+iblMaxMipLevel when iblMaxMipLevel is 4.
// We found empirically that this mapping works very well for a 256 cubemap with 5 levels used,
// but also scales well for other iblMaxMipLevel values.
const float a = 2.0f;
const float b = -1.0f;
return (lod != 0.0) ? saturate((sqrt(a * a + 4.0 * b * lod) - a) / (2.0 * b)) : 0.0;
}
void postProcess(inout PostProcessInputs postProcess) {
vec2 uv = variable_vertex.xy; // interpolated at pixel's center
float lod = floor(uv.x);
uint index = uint(uv.y);
float perceptualRoughness = lodToPerceptualRoughness(saturate(lod * materialParams.oneOverLevelsMinusOne));
float roughness = perceptualRoughness * perceptualRoughness;
postProcess.weight = computeWeight(index, roughness);
}
}