diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 03230ba863..edf23fe7fc 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -5,6 +5,9 @@ A new header is inserted each time a *tag* is created. ## v1.11.3 (currently main branch) +- engine: Option to automatically compute bent normals from SSAO & apply to specular AO + [⚠️ **Material breakage**]. + ## v1.11.2 - engine: New API: `ColorGrading::Builder::toneMapper(const ToneMapper*)`. diff --git a/android/filament-android/src/main/cpp/View.cpp b/android/filament-android/src/main/cpp/View.cpp index c37c187648..6b132978d6 100644 --- a/android/filament-android/src/main/cpp/View.cpp +++ b/android/filament-android/src/main/cpp/View.cpp @@ -217,7 +217,8 @@ extern "C" JNIEXPORT void JNICALL Java_com_google_android_filament_View_nSetAmbientOcclusionOptions(JNIEnv*, jclass, jlong nativeView, jfloat radius, jfloat bias, jfloat power, jfloat resolution, jfloat intensity, jfloat bilateralThreshold, - jint quality, jint lowPassFilter, jint upsampling, jboolean enabled, jfloat minHorizonAngleRad) { + jint quality, jint lowPassFilter, jint upsampling, jboolean enabled, jboolean bentNormals, + jfloat minHorizonAngleRad) { View* view = (View*) nativeView; View::AmbientOcclusionOptions options = view->getAmbientOcclusionOptions(); options.radius = radius; @@ -230,6 +231,7 @@ Java_com_google_android_filament_View_nSetAmbientOcclusionOptions(JNIEnv*, jclas options.lowPassFilter = (View::QualityLevel)lowPassFilter; options.upsampling = (View::QualityLevel)upsampling; options.enabled = (bool)enabled; + options.bentNormals = (bool)bentNormals; options.minHorizonAngleRad = minHorizonAngleRad; view->setAmbientOcclusionOptions(options); } diff --git a/android/filament-android/src/main/java/com/google/android/filament/View.java b/android/filament-android/src/main/java/com/google/android/filament/View.java index 73993164da..21c774e168 100644 --- a/android/filament-android/src/main/java/com/google/android/filament/View.java +++ b/android/filament-android/src/main/java/com/google/android/filament/View.java @@ -208,6 +208,11 @@ public class View { */ public boolean enabled = false; + /** + * enables bent normals computation from AO, and specular AO + */ + public boolean bentNormals = false; + /** * Minimal angle to consider in radian. This is used to reduce the creases that can * appear due to insufficiently tessellated geometry. @@ -1390,7 +1395,7 @@ public class View { nSetAmbientOcclusionOptions(getNativeObject(), options.radius, options.bias, options.power, options.resolution, options.intensity, options.bilateralThreshold, options.quality.ordinal(), options.lowPassFilter.ordinal(), options.upsampling.ordinal(), - options.enabled, options.minHorizonAngleRad); + options.enabled, options.bentNormals, options.minHorizonAngleRad); nSetSSCTOptions(getNativeObject(), options.ssctLightConeRad, options.ssctStartTraceDistance, options.ssctContactDistanceMax, options.ssctIntensity, options.ssctLightDirection[0], options.ssctLightDirection[1], options.ssctLightDirection[2], @@ -1567,7 +1572,7 @@ public class View { private static native boolean nIsFrontFaceWindingInverted(long nativeView); private static native void nSetAmbientOcclusion(long nativeView, int ordinal); private static native int nGetAmbientOcclusion(long nativeView); - private static native void nSetAmbientOcclusionOptions(long nativeView, float radius, float bias, float power, float resolution, float intensity, float bilateralThreshold, int quality, int lowPassFilter, int upsampling, boolean enabled, float minHorizonAngleRad); + private static native void nSetAmbientOcclusionOptions(long nativeView, float radius, float bias, float power, float resolution, float intensity, float bilateralThreshold, int quality, int lowPassFilter, int upsampling, boolean enabled, boolean bentNormals, float minHorizonAngleRad); private static native void nSetSSCTOptions(long nativeView, float ssctLightConeRad, float ssctStartTraceDistance, float ssctContactDistanceMax, float ssctIntensity, float v, float v1, float v2, float ssctDepthBias, float ssctDepthSlopeBias, int ssctSampleCount, int ssctRayCount, boolean ssctEnabled); private static native void nSetBloomOptions(long nativeView, long dirtNativeObject, float dirtStrength, float strength, int resolution, float anamorphism, int levels, int blendMode, boolean threshold, boolean enabled, float highlight, boolean lensFlare, boolean starburst, float chromaticAberration, int ghostCount, float ghostSpacing, float ghostThreshold, float haloThickness, float haloRadius, float haloThreshold); diff --git a/filament/CMakeLists.txt b/filament/CMakeLists.txt index 807d25e106..78480e7a70 100644 --- a/filament/CMakeLists.txt +++ b/filament/CMakeLists.txt @@ -179,9 +179,11 @@ set(MATERIAL_SRCS src/materials/bloom/bloomDownsample.mat src/materials/bloom/bloomUpsample.mat src/materials/ssao/bilateralBlur.mat + src/materials/ssao/bilateralBlurBentNormals.mat src/materials/ssao/mipmapDepth.mat src/materials/skybox.mat src/materials/ssao/sao.mat + src/materials/ssao/saoBentNormals.mat src/materials/separableGaussianBlur.mat src/materials/antiAliasing/fxaa.mat src/materials/antiAliasing/taa.mat @@ -322,12 +324,29 @@ add_custom_command( APPEND ) +add_custom_command( + OUTPUT "${MATERIAL_DIR}/saoBentNormals.filamat" + DEPENDS src/materials/ssao/ssaoUtils.fs + DEPENDS src/materials/ssao/ssct.fs + DEPENDS src/materials/ssao/depthUtils.fs + DEPENDS src/materials/ssao/geometry.fs + DEPENDS src/materials/ssao/saoImpl.fs + DEPENDS src/materials/ssao/ssctImpl.fs + APPEND +) + add_custom_command( OUTPUT "${MATERIAL_DIR}/bilateralBlur.filamat" DEPENDS src/materials/ssao/ssaoUtils.fs APPEND ) +add_custom_command( + OUTPUT "${MATERIAL_DIR}/bilateralBlurBentNormals.filamat" + DEPENDS src/materials/ssao/ssaoUtils.fs + APPEND +) + add_custom_command( OUTPUT ${RESGEN_OUTPUTS} COMMAND resgen ${RESGEN_FLAGS} ${MATERIAL_BINS} diff --git a/filament/backend/src/opengl/OpenGLDriver.cpp b/filament/backend/src/opengl/OpenGLDriver.cpp index e3a45eb302..1cab1ecc00 100644 --- a/filament/backend/src/opengl/OpenGLDriver.cpp +++ b/filament/backend/src/opengl/OpenGLDriver.cpp @@ -3026,7 +3026,7 @@ void OpenGLDriver::blit(TargetBufferFlags buffers, glFilterMode = GL_NEAREST; } - // note: for msaa RenderTargets withh non-msaa attachments, we copy from the msaa sidecar + // note: for msaa RenderTargets with non-msaa attachments, we copy from the msaa sidecar // buffer -- this should produce the same output that if we copied from the resolved // texture. EXT_multisampled_render_to_texture seems to allow both behaviours, and this // is an emulation of that. We cannot use the resolved texture easily because it's not diff --git a/filament/include/filament/View.h b/filament/include/filament/View.h index ce8eb0c711..7b0c1f6019 100644 --- a/filament/include/filament/View.h +++ b/filament/include/filament/View.h @@ -288,6 +288,7 @@ public: QualityLevel lowPassFilter = QualityLevel::MEDIUM; //!< affects AO smoothness QualityLevel upsampling = QualityLevel::LOW; //!< affects AO buffer upsampling quality bool enabled = false; //!< enables or disables screen-space ambient occlusion + bool bentNormals = false; //!< enables bent normals computation from AO, and specular AO float minHorizonAngleRad = 0.0f; //!< min angle in radian to consider /** * Screen Space Cone Tracing (SSCT) options diff --git a/filament/src/PostProcessManager.cpp b/filament/src/PostProcessManager.cpp index 0dc2205119..59e700cf8f 100644 --- a/filament/src/PostProcessManager.cpp +++ b/filament/src/PostProcessManager.cpp @@ -201,6 +201,7 @@ struct MaterialInfo { static const MaterialInfo sMaterialList[] = { { "bilateralBlur", MATERIAL(BILATERALBLUR) }, + { "bilateralBlurBentNormals", MATERIAL(BILATERALBLURBENTNORMALS) }, { "blitHigh", MATERIAL(BLITHIGH) }, { "blitLow", MATERIAL(BLITLOW) }, { "blitMedium", MATERIAL(BLITMEDIUM) }, @@ -221,6 +222,7 @@ static const MaterialInfo sMaterialList[] = { { "fxaa", MATERIAL(FXAA) }, { "mipmapDepth", MATERIAL(MIPMAPDEPTH) }, { "sao", MATERIAL(SAO) }, + { "saoBentNormals", MATERIAL(SAOBENTNORMALS) }, { "separableGaussianBlur", MATERIAL(SEPARABLEGAUSSIANBLUR) }, { "taa", MATERIAL(TAA) }, { "vsmMipmap", MATERIAL(VSMMIPMAP) }, @@ -399,7 +401,8 @@ FrameGraphId PostProcessManager::screenSpaceAmbientOcclusion( // (see en.wikipedia.org/wiki/Gaussian_filter) // More intuitively, 2q is the width of the filter in pixels. BilateralPassConfig config = { - .bilateralThreshold = options.bilateralThreshold + .bentNormals = options.bentNormals, + .bilateralThreshold = options.bilateralThreshold, }; float sampleCount{}; @@ -452,8 +455,12 @@ FrameGraphId PostProcessManager::screenSpaceAmbientOcclusion( struct SSAOPassData { FrameGraphId depth; FrameGraphId ssao; + FrameGraphId ao; + FrameGraphId bn; }; + const bool computeBentNormals = options.bentNormals; + const bool highQualityUpsampling = options.upsampling >= View::QualityLevel::HIGH && options.resolution < 1.0f; @@ -467,29 +474,39 @@ FrameGraphId PostProcessManager::screenSpaceAmbientOcclusion( data.ssao = builder.createTexture("SSAO Buffer", { .width = desc.width, .height = desc.height, - .depth = 1, + .depth = computeBentNormals ? 2u : 1u, .type = Texture::Sampler::SAMPLER_2D_ARRAY, - .format = (lowPassFilterEnabled || highQualityUpsampling) ? TextureFormat::RGB8 : TextureFormat::R8 + .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); - data.ssao = builder.write(data.ssao, FrameGraphTexture::Usage::COLOR_ATTACHMENT); builder.declareRenderPass("SSAO Target", { - .attachments = { .color = { data.ssao }, .depth = data.depth }, + .attachments = { .color = { data.ao, data.bn }, .depth = data.depth }, .clearColor = { 1.0f }, - .clearFlags = TargetBufferFlags::COLOR + .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.ssao); + 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( @@ -512,7 +529,10 @@ FrameGraphId PostProcessManager::screenSpaceAmbientOcclusion( 0.0, 0.0, 0.0, 1.0 }}; - auto& material = getPostProcessMaterial("sao"); + auto& material = computeBentNormals ? + getPostProcessMaterial("saoBentNormals") : + getPostProcessMaterial("sao"); + FMaterialInstance* const mi = material.getMaterialInstance(); mi->setParameter("depth", depth, { .filterMin = SamplerMinFilter::NEAREST_MIPMAP_NEAREST }); @@ -584,7 +604,7 @@ FrameGraphId PostProcessManager::screenSpaceAmbientOcclusion( config); ssao = bilateralBlurPass(fg, ssao, { 0, config.scale }, cameraInfo.zf, - highQualityUpsampling ? TextureFormat::RGB8 : TextureFormat::R8, + (highQualityUpsampling || computeBentNormals) ? TextureFormat::RGB8 : TextureFormat::R8, config); } @@ -601,6 +621,8 @@ FrameGraphId PostProcessManager::bilateralBlurPass( struct BlurPassData { FrameGraphId input; FrameGraphId blurred; + FrameGraphId ao; + FrameGraphId bn; }; auto& blurPass = fg.addPass("Separable Blur Pass", @@ -621,13 +643,24 @@ FrameGraphId PostProcessManager::bilateralBlurPass( 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.blurred }, .depth = depth }, + .attachments = { .color = { data.ao, data.bn }, .depth = depth }, .clearColor = { 1.0f }, - .clearFlags = TargetBufferFlags::COLOR + .clearFlags = TargetBufferFlags::COLOR0 | TargetBufferFlags::COLOR1 }); }, [=](FrameGraphResources const& resources, @@ -653,7 +686,9 @@ FrameGraphId PostProcessManager::bilateralBlurPass( uint32_t kGaussianCount = gaussianKernel(kGaussianSamples, config.kernelSize, config.standardDeviation); - auto& material = getPostProcessMaterial("bilateralBlur"); + 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}); diff --git a/filament/src/PostProcessManager.h b/filament/src/PostProcessManager.h index 8b3f888b1b..a1928d3f69 100644 --- a/filament/src/PostProcessManager.h +++ b/filament/src/PostProcessManager.h @@ -152,6 +152,7 @@ private: struct BilateralPassConfig { uint8_t kernelSize = 11; + bool bentNormals = false; float standardDeviation = 1.0f; float bilateralThreshold = 0.0625f; float scale = 1.0f; diff --git a/filament/src/View.cpp b/filament/src/View.cpp index 617924f70f..99e2bd8bbe 100644 --- a/filament/src/View.cpp +++ b/filament/src/View.cpp @@ -682,10 +682,12 @@ void FView::prepareSSAO(Handle ssao) const noexcept { SamplerMagFilter::LINEAR : SamplerMagFilter::NEAREST }); - const float edgeDistance = 1.0 / 0.0625;// TODO: don't hardcode this + const float edgeDistance = 1.0f / mAmbientOcclusionOptions.bilateralThreshold; auto& s = mPerViewUb.edit(); s.aoSamplingQualityAndEdgeDistance = mAmbientOcclusionOptions.enabled && highQualitySampling ? edgeDistance : 0.0f; + s.aoBentNormals = + mAmbientOcclusionOptions.enabled && mAmbientOcclusionOptions.bentNormals ? 1.0f : 0.0f; } void FView::prepareSSR(backend::Handle ssr, float refractionLodOffset) const noexcept { diff --git a/filament/src/materials/ssao/bilateralBlur.mat b/filament/src/materials/ssao/bilateralBlur.mat index 15783f726c..f311c20c93 100644 --- a/filament/src/materials/ssao/bilateralBlur.mat +++ b/filament/src/materials/ssao/bilateralBlur.mat @@ -54,8 +54,7 @@ fragment { vec3 data = textureLod(saoTexture, vec3(position, 0.0), 0.0).rgb; // bilateral sample - float bilateral = bilateralWeight(depth, unpack(data.gb)); - bilateral *= weight; + float bilateral = weight * bilateralWeight(depth, unpack(data.gb)); sum += data.r * bilateral; totalWeight += bilateral; } diff --git a/filament/src/materials/ssao/bilateralBlurBentNormals.mat b/filament/src/materials/ssao/bilateralBlurBentNormals.mat new file mode 100644 index 0000000000..b980815899 --- /dev/null +++ b/filament/src/materials/ssao/bilateralBlurBentNormals.mat @@ -0,0 +1,135 @@ +material { + name : bilateralBlurBentNormals, + parameters : [ + { + type : sampler2dArray, + name : ssao, + precision: medium + }, + { + type : float2, + name : axis, + precision: high + }, + { + type : int, + name : sampleCount + }, + { + type : float, + name : farPlaneOverEdgeDistance + }, + { + type : float[16], + name : kernel + } + ], + outputs : [ + { + name : aoData, + target : color, + type : float3 + }, + { + name : bnData, + target : color, + type : float3 + } + ], + variables : [ + vertex + ], + domain : postprocess, + depthWrite : false, + depthCulling : false +} + +vertex { + void postProcessVertex(inout PostProcessVertexInputs postProcess) { + postProcess.vertex.xy = postProcess.normalizedUV; + } +} + +fragment { + #include "ssaoUtils.fs" + + void dummy(){} + + float bilateralWeight(in highp float depth, in highp float sampleDepth) { + float diff = (sampleDepth - depth) * materialParams.farPlaneOverEdgeDistance; + return max(0.0, 1.0 - diff * diff); + } + + void tapAO(const highp sampler2DArray saoTexture, highp vec2 uv, + out float ao, out highp float sampleDepth) { + vec3 data = textureLod(saoTexture, vec3(uv, 0.0), 0.0).rgb; + ao = data.r; + sampleDepth = unpack(data.gb); + } + + void tapBN(const highp sampler2DArray saoTexture, highp vec2 uv, + out vec3 bentNormal) { + vec3 data = textureLod(saoTexture, vec3(uv, 1.0), 0.0).xyz; + bentNormal = unpackBentNormal(data); + } + + void postProcess(inout PostProcessInputs postProcess) { + highp vec2 uv = variable_vertex.xy; // interpolated at pixel's center + + float ao; + highp float depth; + highp float sampleDepth; + vec3 bn; + + vec3 data = textureLod(materialParams_ssao, vec3(uv, 0.0), 0.0).rgb; + ao = data.r; + depth = unpack(data.gb); + + if (data.g * data.b == 1.0) { + // This is the skybox, skip + postProcess.aoData = data; + postProcess.bnData = vec3(0.0); + return; + } + + tapBN(materialParams_ssao, uv, bn); + + // we handle the center pixel separately because it doesn't participate in + // bilateral filtering + float totalWeight = materialParams.kernel[0]; + float sumAO = ao * totalWeight; + vec3 sumBN = bn * totalWeight; + + vec2 offset = materialParams.axis; + for (int i = 1; i < materialParams.sampleCount; i++) { + float weight = materialParams.kernel[i]; + float bilateral; + + tapAO(materialParams_ssao, uv + offset, ao, sampleDepth); + bilateral = weight * bilateralWeight(depth, sampleDepth); + totalWeight += bilateral; + tapBN(materialParams_ssao, uv + offset, bn); + sumAO += ao * bilateral; + sumBN += bn * bilateral; + + tapAO(materialParams_ssao, uv - offset, ao, sampleDepth); + bilateral = weight * bilateralWeight(depth, sampleDepth); + totalWeight += bilateral; + tapBN(materialParams_ssao, uv - offset, bn); + sumAO += ao * bilateral; + sumBN += bn * bilateral; + + offset += materialParams.axis; + } + + ao = sumAO * (1.0 / totalWeight); + bn = sumBN * (1.0 / totalWeight); + + // simple dithering helps a lot (assumes 8 bits target) + // this is most useful with high quality/large blurs + ao += ((random(gl_FragCoord.xy) - 0.5) / 255.0); + + postProcess.aoData = vec3(ao, data.gb); + postProcess.bnData = packBentNormal(bn); + } +} diff --git a/filament/src/materials/ssao/sao.mat b/filament/src/materials/ssao/sao.mat index bfc95e7c44..065d598c2c 100644 --- a/filament/src/materials/ssao/sao.mat +++ b/filament/src/materials/ssao/sao.mat @@ -135,6 +135,9 @@ vertex { } fragment { + +#define COMPUTE_BENT_NORMAL 0 + #include "saoImpl.fs" #include "ssctImpl.fs" #include "geometry.fs" @@ -155,9 +158,10 @@ fragment { materialParams.depthParams); float occlusion = 0.0; + vec3 bentNormal; // will be discarded if (materialParams.intensity > 0.0) { - occlusion = scalableAmbientObscurance(uv, origin, normal); + scalableAmbientObscurance(occlusion, bentNormal, uv, origin, normal); } if (materialParams.ssctIntensity > 0.0) { diff --git a/filament/src/materials/ssao/saoBentNormals.mat b/filament/src/materials/ssao/saoBentNormals.mat new file mode 100644 index 0000000000..2055014777 --- /dev/null +++ b/filament/src/materials/ssao/saoBentNormals.mat @@ -0,0 +1,197 @@ +material { + name : saoBentNormals, + parameters : [ + { + type : sampler2d, + name : depth, + precision: high + }, + { + type : mat4, + name : screenFromViewMatrix + }, + { + type : float4, + name : resolution, + precision: high + }, + { + type : float2, + name : positionParams, + precision: high + }, + { + type : float, + name : depthParams, + precision: high + }, + { + type : float, + name : invRadiusSquared + }, + { + type : float, + name : minHorizonAngleSineSquared + }, + { + type : float, + name : peak2 + }, + { + type : float, + name : projectionScale + }, + { + type : float, + name : projectionScaleRadius + }, + { + type : float, + name : bias + }, + { + type : float, + name : power + }, + { + type : float, + name : intensity + }, + { + type : float, + name : spiralTurns + }, + { + type : float2, + name : sampleCount + }, + { + type : float2, + name : angleIncCosSin + }, + { + type : float, + name : invFarPlane + }, + { + type : int, + name : maxLevel + }, + { + type : float2, + name : reserved + }, + { + type : float, + name : ssctShadowDistance + }, + { + type : float, + name : ssctConeAngleTangeant + }, + { + type : float, + name : ssctContactDistanceMaxInv + }, + { + type : float3, + name : ssctVsLightDirection + }, + { + type : float, + name : ssctIntensity + }, + { + type : float2, + name : ssctDepthBias + }, + { + type : float2, + name : ssctRayCount + }, + { + type : uint, + name : ssctSampleCount + } + ], + outputs : [ + { + name : aoData, + target : color, + type : float3 + }, + { + name : bnData, + target : color, + type : float3 + } + ], + variables : [ + vertex + ], + domain : postprocess, + depthWrite : false, + depthCulling : true +} + +vertex { + void postProcessVertex(inout PostProcessVertexInputs postProcess) { + postProcess.vertex.xy = postProcess.normalizedUV; +#if defined(TARGET_METAL_ENVIRONMENT) || defined(TARGET_VULKAN_ENVIRONMENT) + // On metal/vulkan postProcess.normalizedUV has its origin at the left-top, but we need a + // uniform coordinate space for SAO. So, we flip the y coordinate so we have bottom-left UV + // coordinates across all backends. + postProcess.vertex.y = 1.0 - postProcess.vertex.y; +#endif + } +} + +fragment { + +#define COMPUTE_BENT_NORMAL 1 + + #include "saoImpl.fs" + #include "ssctImpl.fs" + #include "geometry.fs" + #include "ssaoUtils.fs" + + void dummy(){} + + void postProcess(inout PostProcessInputs postProcess) { + highp vec2 uv = variable_vertex.xy; // interpolated to pixel center + + highp float depth = sampleDepth(materialParams_depth, uv, 0.0); + highp float z = linearizeDepth(depth, materialParams.depthParams); + highp vec3 origin = computeViewSpacePositionFromDepth(uv, z, materialParams.positionParams); + + vec3 normal = computeViewSpaceNormal(materialParams_depth, uv, depth, origin, + materialParams.resolution, + materialParams.positionParams, + materialParams.depthParams); + + float occlusion = 0.0; + vec3 bentNormal = normal; + + if (materialParams.intensity > 0.0) { + scalableAmbientObscurance(occlusion, bentNormal, uv, origin, normal); + } + + if (materialParams.ssctIntensity > 0.0) { + occlusion = max(occlusion, dominantLightShadowing(uv, origin, normal)); + } + + // occlusion to visibility + float aoVisibility = pow(saturate(1.0 - occlusion), materialParams.power); + +#if defined(TARGET_MOBILE) + // this line is needed to workaround what seems to be a bug on qualcomm hardware + aoVisibility += gl_FragCoord.x * MEDIUMP_FLT_MIN; +#endif + + // transform to world space (we're guaranteed the view matrix is a rigid body transform) + vec3 bn = mat3(getWorldFromViewMatrix()) * bentNormal; + + postProcess.aoData = vec3(aoVisibility, pack(origin.z * materialParams.invFarPlane)); + postProcess.bnData = packBentNormal(bn); + } +} diff --git a/filament/src/materials/ssao/saoImpl.fs b/filament/src/materials/ssao/saoImpl.fs index a7b39dd41a..ee0d14e9c1 100644 --- a/filament/src/materials/ssao/saoImpl.fs +++ b/filament/src/materials/ssao/saoImpl.fs @@ -24,6 +24,10 @@ #include "ssaoUtils.fs" #include "geometry.fs" +#ifndef COMPUTE_BENT_NORMAL +#error "COMPUTE_BENT_NORMAL must be set" +#endif + const float kLog2LodRate = 3.0; // Ambient Occlusion, largely inspired from: @@ -52,7 +56,8 @@ vec3 tapLocationFast(float i, vec2 p, const float noise) { return vec3(p, radius * radius); } -void computeAmbientOcclusionSAO(inout float occlusion, float i, float ssDiskRadius, +void computeAmbientOcclusionSAO(inout float occlusion, inout vec3 bentNormal, + float i, float ssDiskRadius, const highp vec2 uv, const highp vec3 origin, const vec3 normal, const vec2 tapPosition, const float noise) { @@ -82,10 +87,26 @@ void computeAmbientOcclusionSAO(inout float occlusion, float i, float ssDiskRadi // sin(beta) * |v|. So the test simplifies to vn^2 < vv * sin(epsilon)^2. w *= step(vv * materialParams.minHorizonAngleSineSquared, vn * vn); - occlusion += w * max(0.0, vn + origin.z * materialParams.bias) / (vv + materialParams.peak2); + float sampleOcclusion = max(0.0, vn + (origin.z * materialParams.bias)) / (vv + materialParams.peak2); + occlusion += w * sampleOcclusion; + +#if COMPUTE_BENT_NORMAL + + // TODO: revisit how we choose to keep the normal or not + // reject samples beyond the far plane + if (occlusionDepth * materialParams.invFarPlane < 1.0) { + float rr = 1.0 / materialParams.invRadiusSquared; + float cc = vv - vn*vn; + float s = sqrt(max(0.0, rr - cc)); + vec3 n = normalize(v + normal * (s - vn));// vn is negative + bentNormal += n * (sampleOcclusion <= 0.0 ? 1.0 : 0.0); + } + +#endif } -float scalableAmbientObscurance(highp vec2 uv, highp vec3 origin, vec3 normal) { +void scalableAmbientObscurance(out float obscurance, out vec3 bentNormal, + highp vec2 uv, highp vec3 origin, vec3 normal) { float noise = random(getFragCoord(materialParams.resolution)); highp vec2 tapPosition = startPosition(noise); highp mat2 angleStep = tapAngleStep(); @@ -94,10 +115,15 @@ float scalableAmbientObscurance(highp vec2 uv, highp vec3 origin, vec3 normal) { // proportional to the projected area of the sphere float ssDiskRadius = -(materialParams.projectionScaleRadius / origin.z); - float occlusion = 0.0; + obscurance = 0.0; + bentNormal = normal; for (float i = 0.0; i < materialParams.sampleCount.x; i += 1.0) { - computeAmbientOcclusionSAO(occlusion, i, ssDiskRadius, uv, origin, normal, tapPosition, noise); + computeAmbientOcclusionSAO(obscurance, bentNormal, + i, ssDiskRadius, uv, origin, normal, tapPosition, noise); tapPosition = angleStep * tapPosition; } - return sqrt(occlusion * materialParams.intensity); + obscurance = sqrt(obscurance * materialParams.intensity); +#if COMPUTE_BENT_NORMAL + bentNormal = normalize(bentNormal); +#endif } diff --git a/filament/src/materials/ssao/ssaoUtils.fs b/filament/src/materials/ssao/ssaoUtils.fs index 8ec668bcf0..6ec84ea9e5 100644 --- a/filament/src/materials/ssao/ssaoUtils.fs +++ b/filament/src/materials/ssao/ssaoUtils.fs @@ -50,5 +50,14 @@ highp float unpack(highp vec2 depth) { return (depth.x * (256.0 / 257.0) + depth.y * (1.0 / 257.0)); } +vec3 packBentNormal(vec3 bn) { + return bn * 0.5 + 0.5; +} + +vec3 unpackBentNormal(vec3 bn) { + return bn * 2.0 - 1.0; +} + + #endif // FILAMENT_MATERIALS_SSAO_UTILS diff --git a/libs/filabridge/include/filament/MaterialEnums.h b/libs/filabridge/include/filament/MaterialEnums.h index 249ebf259a..4a7a35e8ca 100644 --- a/libs/filabridge/include/filament/MaterialEnums.h +++ b/libs/filabridge/include/filament/MaterialEnums.h @@ -27,7 +27,7 @@ namespace filament { // update this when a new version of filament wouldn't work with older materials -static constexpr size_t MATERIAL_VERSION = 11; +static constexpr size_t MATERIAL_VERSION = 12; /** * Supported shading models diff --git a/libs/filabridge/include/private/filament/UibStructs.h b/libs/filabridge/include/private/filament/UibStructs.h index e276474c58..055e74d882 100644 --- a/libs/filabridge/include/private/filament/UibStructs.h +++ b/libs/filabridge/include/private/filament/UibStructs.h @@ -115,7 +115,7 @@ struct PerViewUib { // NOLINT(cppcoreguidelines-pro-type-member-init) uint32_t cascades; float aoSamplingQualityAndEdgeDistance; // 0: bilinear, !0: bilateral edge distance - float aoReserved1; + float aoBentNormals; // 0: no AO bent normal, >0.0 AO bent normals float aoReserved2; float aoReserved3; diff --git a/libs/filamat/src/UibGenerator.cpp b/libs/filamat/src/UibGenerator.cpp index 235eece829..e87bc057db 100644 --- a/libs/filamat/src/UibGenerator.cpp +++ b/libs/filamat/src/UibGenerator.cpp @@ -96,7 +96,7 @@ UniformInterfaceBlock const& UibGenerator::getPerViewUib() noexcept { // SSAO sampling parameters .add("aoSamplingQualityAndEdgeDistance", 1, UniformInterfaceBlock::Type::FLOAT) - .add("aoReserved1", 1, UniformInterfaceBlock::Type::FLOAT) + .add("aoBentNormals", 1, UniformInterfaceBlock::Type::FLOAT) .add("aoReserved2", 1, UniformInterfaceBlock::Type::FLOAT) .add("aoReserved3", 1, UniformInterfaceBlock::Type::FLOAT) diff --git a/libs/viewer/src/Settings.cpp b/libs/viewer/src/Settings.cpp index 37a6063248..a255f5b6e4 100644 --- a/libs/viewer/src/Settings.cpp +++ b/libs/viewer/src/Settings.cpp @@ -428,6 +428,8 @@ static int parse(jsmntok_t const* tokens, int i, const char* jsonChunk, i = parse(tokens, i + 1, jsonChunk, &out->upsampling); } else if (compare(tok, jsonChunk, "enabled") == 0) { i = parse(tokens, i + 1, jsonChunk, &out->enabled); + } else if (compare(tok, jsonChunk, "bentNormals") == 0) { + i = parse(tokens, i + 1, jsonChunk, &out->bentNormals); } else if (compare(tok, jsonChunk, "minHorizonAngleRad") == 0) { i = parse(tokens, i + 1, jsonChunk, &out->minHorizonAngleRad); } else if (compare(tok, jsonChunk, "ssct") == 0) { @@ -1204,6 +1206,7 @@ static std::ostream& operator<<(std::ostream& out, const AmbientOcclusionOptions << "\"lowPassFilter\": " << (in.lowPassFilter) << ",\n" << "\"upsampling\": " << (in.upsampling) << ",\n" << "\"enabled\": " << to_string(in.enabled) << ",\n" + << "\"bentNormals\": " << to_string(in.bentNormals) << ",\n" << "\"minHorizonAngleRad\": " << (in.minHorizonAngleRad) << ",\n" << "\"ssct\": " << (in.ssct) << "\n" << "}"; diff --git a/libs/viewer/src/SimpleViewer.cpp b/libs/viewer/src/SimpleViewer.cpp index 1a92d45c92..d67f4fd8c3 100644 --- a/libs/viewer/src/SimpleViewer.cpp +++ b/libs/viewer/src/SimpleViewer.cpp @@ -700,6 +700,7 @@ void SimpleViewer::updateUserInterface() { ImGui::SliderInt("Quality", &quality, 0, 3); ImGui::SliderInt("Low Pass", &lowpass, 0, 2); + ImGui::Checkbox("Bent Normals", &ssao.bentNormals); ImGui::Checkbox("High quality upsampling", &upsampling); ImGui::SliderFloat("Min Horizon angle", &ssao.minHorizonAngleRad, 0.0f, (float)M_PI_4); diff --git a/samples/material_sandbox.cpp b/samples/material_sandbox.cpp index a0aa39ac7b..58ee66ef9c 100644 --- a/samples/material_sandbox.cpp +++ b/samples/material_sandbox.cpp @@ -620,6 +620,8 @@ static void gui(filament::Engine* engine, filament::View*) { ImGui::SliderFloat("Power", ¶ms.ssaoOptions.power, 0.0f, 4.0f); ImGui::SliderInt("Quality", &quality, 0, 3); ImGui::SliderInt("Low Pass", &lowpass, 0, 2); + ImGui::SliderFloat("Bilateral Threshold", ¶ms.ssaoOptions.bilateralThreshold, 0.0f, 0.5f); + ImGui::Checkbox("Bent Normals", ¶ms.ssaoOptions.bentNormals); ImGui::Checkbox("High quality upsampling", &upsampling); params.ssaoOptions.upsampling = upsampling ? View::QualityLevel::HIGH : View::QualityLevel::LOW; params.ssaoOptions.quality = (View::QualityLevel)quality; diff --git a/samples/sample_full_pbr.cpp b/samples/sample_full_pbr.cpp index 2e12c5e1cf..dcc061e4d3 100644 --- a/samples/sample_full_pbr.cpp +++ b/samples/sample_full_pbr.cpp @@ -241,6 +241,15 @@ static void setup(Engine* engine, View* view, Scene* scene) { Path path(g_pbrConfig.materialDir); std::string name(path.getName()); + view->setAmbientOcclusionOptions({ + .radius = 0.01f, + .bilateralThreshold = 0.005f, + .quality = View::QualityLevel::ULTRA, + .lowPassFilter = View::QualityLevel::MEDIUM, + .upsampling = View::QualityLevel::HIGH, + .enabled = true + }); + bool hasUV = false; for (auto& map: g_maps) { loadTexture(engine, path.concat(name + map.suffix + ".png"), &map.texture, map.sRGB); @@ -377,6 +386,7 @@ static void setup(Engine* engine, View* view, Scene* scene) { .targetApi(MaterialBuilder::TargetApi::ALL) #ifndef NDEBUG .optimization(MaterialBuilderBase::Optimization::NONE) + .generateDebugInfo(true) #endif .material(shader.c_str()) .multiBounceAmbientOcclusion(true) diff --git a/shaders/src/ambient_occlusion.fs b/shaders/src/ambient_occlusion.fs index fb008dec12..e26ce41b9b 100644 --- a/shaders/src/ambient_occlusion.fs +++ b/shaders/src/ambient_occlusion.fs @@ -16,9 +16,12 @@ float unpack(vec2 depth) { return (depth.x * (256.0 / 257.0) + depth.y * (1.0 / 257.0)); } -float evaluateSSAO() { +struct SSAOInterpolationCache { + highp vec4 weights; +}; + +float evaluateSSAO(const highp vec2 uv, out SSAOInterpolationCache cache) { #if defined(BLEND_MODE_OPAQUE) || defined(BLEND_MODE_MASKED) - highp vec2 uv = uvToRenderTargetUV(getNormalizedViewportCoord().xy); // Upscale the SSAO buffer in real-time, in high quality mode we use a custom bilinear // filter. This adds about 2.0ms @ 250MHz on Pixel 4. @@ -40,6 +43,14 @@ float evaluateSSAO() { vec4 dg = vec4(s01.g, s11.g, s10.g, s00.g); vec4 db = vec4(s01.b, s11.b, s10.b, s00.b); #endif + // bilateral weights + vec4 depths; + depths.x = unpack(vec2(dg.x, db.x)); + depths.y = unpack(vec2(dg.y, db.y)); + depths.z = unpack(vec2(dg.z, db.z)); + depths.w = unpack(vec2(dg.w, db.w)); + depths *= -frameUniforms.cameraFar; + // bilinear weights vec2 f = fract(uv * size - 0.5); vec4 b; @@ -48,18 +59,12 @@ float evaluateSSAO() { b.z = f.x * (1.0 - f.y); b.w = (1.0 - f.x) * (1.0 - f.y); - // bilateral weights - vec4 depths; - depths.x = unpack(vec2(dg.x, db.x)); - depths.y = unpack(vec2(dg.y, db.y)); - depths.z = unpack(vec2(dg.z, db.z)); - depths.w = unpack(vec2(dg.w, db.w)); - depths *= -frameUniforms.cameraFar; highp mat4 m = getViewFromWorldMatrix(); highp float d = dot(vec3(m[0].z, m[1].z, m[2].z), shading_position) + m[3].z; highp vec4 w = (vec4(d) - depths) * frameUniforms.aoSamplingQualityAndEdgeDistance; w = max(vec4(MEDIUMP_FLT_MIN), 1.0 - w * w) * b; - return dot(ao, w) * (1.0 / (w.x + w.y + w.z + w.w)); + cache.weights = w / (w.x + w.y + w.z + w.w); + return dot(ao, cache.weights); } else { return textureLod(light_ssao, vec3(uv, 0.0), 0.0).r; } @@ -74,7 +79,6 @@ float SpecularAO_Lagarde(float NoV, float visibility, float roughness) { return saturate(pow(NoV + visibility, exp2(-16.0 * roughness - 1.0)) - 1.0 + visibility); } -#if defined(MATERIAL_HAS_BENT_NORMAL) float sphericalCapsIntersection(float cosCap1, float cosCap2, float cosDistance) { // Oat and Sander 2007, "Ambient Aperture Lighting" // Approximation mentioned by Jimenez et al. 2016 @@ -99,11 +103,9 @@ float sphericalCapsIntersection(float cosCap1, float cosCap2, float cosDistance) float area = sq(x) * (-2.0 * x + 3.0); return area * (1.0 - max(cosCap1, cosCap2)); } -#endif // This function could (should?) be implemented as a 3D LUT instead, but we need to save samplers -float SpecularAO_Cones(float NoV, float visibility, float roughness) { -#if defined(MATERIAL_HAS_BENT_NORMAL) +float SpecularAO_Cones(vec3 bentNormal, float NoV, float visibility, float roughness) { // Jimenez et al. 2016, "Practical Realtime Strategies for Accurate Indirect Occlusion" // aperture from ambient occlusion @@ -111,7 +113,7 @@ float SpecularAO_Cones(float NoV, float visibility, float roughness) { // aperture from roughness, log(10) / log(2) = 3.321928 float cosAs = exp2(-3.321928 * sq(roughness)); // angle betwen bent normal and reflection direction - float cosB = dot(shading_bentNormal, shading_reflected); + float cosB = dot(bentNormal, shading_reflected); // Remove the 2 * PI term from the denominator, it cancels out the same term from // sphericalCapsIntersection() @@ -119,22 +121,66 @@ float SpecularAO_Cones(float NoV, float visibility, float roughness) { // Smoothly kill specular AO when entering the perceptual roughness range [0.1..0.3] // Without this, specular AO can remove all reflections, which looks bad on metals return mix(1.0, ao, smoothstep(0.01, 0.09, roughness)); -#else - return SpecularAO_Lagarde(NoV, visibility, roughness); -#endif } /** * Computes a specular occlusion term from the ambient occlusion term. */ -float computeSpecularAO(float NoV, float visibility, float roughness) { + +vec3 unpackBentNormal(vec3 bn) { + // this must match src/materials/ssao/ssaoUtils.fs + return bn * 2.0 - 1.0; +} + +float computeSpecularAO(const highp vec2 uv, float NoV, float visibility, float roughness, + const in SSAOInterpolationCache cache) { + + float specularAO = 1.0; + +// SSAO is not applied when blending is enabled +#if defined(BLEND_MODE_OPAQUE) || defined(BLEND_MODE_MASKED) + #if SPECULAR_AMBIENT_OCCLUSION == SPECULAR_AO_SIMPLE - return SpecularAO_Lagarde(NoV, visibility, roughness); + specularAO = SpecularAO_Lagarde(NoV, visibility, roughness); #elif SPECULAR_AMBIENT_OCCLUSION == SPECULAR_AO_BENT_NORMALS - return SpecularAO_Cones(NoV, visibility, roughness); -#else - return 1.0; +# if defined(MATERIAL_HAS_BENT_NORMAL) + specularAO = SpecularAO_Cones(shading_bentNormal, NoV, visibility, roughness); +# else + specularAO = SpecularAO_Cones(shading_normal, NoV, visibility, roughness); +# endif #endif + + if (frameUniforms.aoBentNormals > 0.0) { + vec3 bn; + if (frameUniforms.aoSamplingQualityAndEdgeDistance > 0.0) { +#if defined(FILAMENT_HAS_FEATURE_TEXTURE_GATHER) + vec4 bnr = textureGather(light_ssao, vec3(uv, 1.0), 0); + vec4 bng = textureGather(light_ssao, vec3(uv, 1.0), 1); + vec4 bnb = textureGather(light_ssao, vec3(uv, 1.0), 2); +#else + vec3 s01 = textureLodOffset(light_ssao, vec3(uv, 1.0), 0.0, ivec2(0, 1)).rgb; + vec3 s11 = textureLodOffset(light_ssao, vec3(uv, 1.0), 0.0, ivec2(1, 1)).rgb; + vec3 s10 = textureLodOffset(light_ssao, vec3(uv, 1.0), 0.0, ivec2(1, 0)).rgb; + vec3 s00 = textureLodOffset(light_ssao, vec3(uv, 1.0), 0.0, ivec2(0, 0)).rgb; + vec4 bnr = vec4(s01.r, s11.r, s10.r, s00.r); + vec4 bng = vec4(s01.g, s11.g, s10.g, s00.g); + vec4 bnb = vec4(s01.b, s11.b, s10.b, s00.b); +#endif + bn.r = dot(bnr, cache.weights); + bn.g = dot(bng, cache.weights); + bn.b = dot(bnb, cache.weights); + } else { + bn = textureLod(light_ssao, vec3(uv, 1.0), 0.0).xyz; + } + bn = unpackBentNormal(bn); + bn = normalize(bn); + specularAO = min(specularAO, SpecularAO_Cones(bn, NoV, visibility, roughness)); + // For now we don't use the AO bent normal for the diffuse because the AO bent normal + // is currently a face normal. + } +#endif + + return specularAO; } #if MULTI_BOUNCE_AMBIENT_OCCLUSION == 1 diff --git a/shaders/src/light_indirect.fs b/shaders/src/light_indirect.fs index 66bddc63da..895a50a0cc 100644 --- a/shaders/src/light_indirect.fs +++ b/shaders/src/light_indirect.fs @@ -573,9 +573,13 @@ void combineDiffuseAndSpecular( } void evaluateIBL(const MaterialInputs material, const PixelParams pixel, inout vec3 color) { - float ssao = evaluateSSAO(); + SSAOInterpolationCache interpolationCache; + + highp vec2 uv = uvToRenderTargetUV(getNormalizedViewportCoord().xy); + float ssao = evaluateSSAO(uv, interpolationCache); float diffuseAO = min(material.ambientOcclusion, ssao); - float specularAO = computeSpecularAO(shading_NoV, diffuseAO, pixel.roughness); + float specularAO = computeSpecularAO(uv, shading_NoV, diffuseAO, pixel.roughness, + interpolationCache); // specular layer vec3 Fr; diff --git a/web/filament-js/jsbindings.cpp b/web/filament-js/jsbindings.cpp index 6503d368bc..1a956185a7 100644 --- a/web/filament-js/jsbindings.cpp +++ b/web/filament-js/jsbindings.cpp @@ -365,7 +365,12 @@ value_object("View$AmbientOcclusionOpti .field("resolution", &filament::View::AmbientOcclusionOptions::resolution) .field("intensity", &filament::View::AmbientOcclusionOptions::intensity) .field("bilateralThreshold", &filament::View::AmbientOcclusionOptions::bilateralThreshold) - .field("quality", &filament::View::AmbientOcclusionOptions::quality); + .field("quality", &filament::View::AmbientOcclusionOptions::quality) + .field("lowPassFilter", &filament::View::AmbientOcclusionOptions::lowPassFilter) + .field("upsampling", &filament::View::AmbientOcclusionOptions::upsampling) + .field("enabled", &filament::View::AmbientOcclusionOptions::enabled) + .field("bentNormals", &filament::View::AmbientOcclusionOptions::bentNormals) + .field("minHorizonAngleRad", &filament::View::AmbientOcclusionOptions::minHorizonAngleRad); // TODO: ssct options value_object("View$DepthOfFieldOptions")