From 0427787cb726e622d0237650dfceca091650e1ac Mon Sep 17 00:00:00 2001 From: Mathias Agopian Date: Tue, 20 Apr 2021 13:23:29 -0700 Subject: [PATCH] Generate the kernel texture on the GPU This avoids having to upload data from the cpu. --- libs/iblprefilter/CMakeLists.txt | 1 + .../iblprefilter/IBLPrefilterContext.h | 4 +- libs/iblprefilter/src/IBLPrefilterContext.cpp | 95 +++++++------- .../src/materials/generateKernel.mat | 118 ++++++++++++++++++ 4 files changed, 167 insertions(+), 51 deletions(-) create mode 100644 libs/iblprefilter/src/materials/generateKernel.mat diff --git a/libs/iblprefilter/CMakeLists.txt b/libs/iblprefilter/CMakeLists.txt index a35dc7b5b1..fb13b8552b 100644 --- a/libs/iblprefilter/CMakeLists.txt +++ b/libs/iblprefilter/CMakeLists.txt @@ -22,6 +22,7 @@ set(PRIVATE_HDRS ) set(MATERIAL_SRCS + src/materials/generateKernel.mat src/materials/iblprefilter.mat ) diff --git a/libs/iblprefilter/include/filament/iblprefilter/IBLPrefilterContext.h b/libs/iblprefilter/include/filament/iblprefilter/IBLPrefilterContext.h index 858891c15e..3ee73afe0f 100644 --- a/libs/iblprefilter/include/filament/iblprefilter/IBLPrefilterContext.h +++ b/libs/iblprefilter/include/filament/iblprefilter/IBLPrefilterContext.h @@ -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 diff --git a/libs/iblprefilter/src/IBLPrefilterContext.cpp b/libs/iblprefilter/src/IBLPrefilterContext.cpp index 8de03f1769..7fd2b2d98e 100644 --- a/libs/iblprefilter/src/IBLPrefilterContext.cpp +++ b/libs/iblprefilter/src/IBLPrefilterContext.cpp @@ -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; } diff --git a/libs/iblprefilter/src/materials/generateKernel.mat b/libs/iblprefilter/src/materials/generateKernel.mat new file mode 100644 index 0000000000..1ef4832202 --- /dev/null +++ b/libs/iblprefilter/src/materials/generateKernel.mat @@ -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); +} + +}