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 06bf7e4dd8..166845d8cc 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 @@ -1949,6 +1949,18 @@ public class View { * Ground Truth-base Ambient Occlusion (GTAO) options */ public float gtaoThicknessHeuristic = 0.004f; + /** + * Ground Truth-base Ambient Occlusion (GTAO) options + */ + public boolean gtaoUseVisibilityBitmasks = false; + /** + * Ground Truth-base Ambient Occlusion (GTAO) options + */ + public float gtaoConstThickness = 0.5f; + /** + * Ground Truth-base Ambient Occlusion (GTAO) options + */ + public boolean gtaoLinearThickness = false; } diff --git a/filament/include/filament/Options.h b/filament/include/filament/Options.h index a7c5a3cf53..07110ee196 100644 --- a/filament/include/filament/Options.h +++ b/filament/include/filament/Options.h @@ -437,7 +437,20 @@ struct AmbientOcclusionOptions { struct Gtao { uint8_t sampleSliceCount = 4; //!< # of slices. Higher value makes less noise. uint8_t sampleStepsPerSlice = 3; //!< # of steps the radius is divided into for integration. Higher value makes less bias. - float thicknessHeuristic = 0.004f; //!< thickness heuristic, should be closed to 0 + float thicknessHeuristic = 0.004f; //!< thickness heuristic, should be closed to 0. No effect when useVisibilityBitmasks sets to true. + + /** + * Enables or disables visibility bitmasks mode. Notes that bent normal doesn't work under this mode. + * Caution: Changing this option at runtime is very expensive as it may trigger a shader re-compilation. + */ + bool useVisibilityBitmasks = false; + float constThickness = 0.5f; //!< constant thickness value of objects on the screen in world space. Only take effect when useVisibilityBitmasks is set to true. + + /** + * Increase thickness with distance to maintain detail on distant surfaces. + * Caution: Changing this option at runtime is very expensive as it may trigger a shader re-compilation. + */ + bool linearThickness = false; }; Gtao gtao; // %codegen_skip_javascript% %codegen_java_flatten% }; diff --git a/filament/src/PostProcessManager.cpp b/filament/src/PostProcessManager.cpp index 74ab961c1a..426770e04c 100644 --- a/filament/src/PostProcessManager.cpp +++ b/filament/src/PostProcessManager.cpp @@ -993,7 +993,17 @@ FrameGraphId PostProcessManager::screenSpaceAmbientOcclusion( #endif auto& material = getPostProcessMaterial(materialName); - FMaterial const * const ma = material.getMaterial(mEngine); + FMaterial* ma = material.getMaterial(mEngine); + bool dirty = false; + setConstantParameter(ma, "useVisibilityBitmasks", options.gtao.useVisibilityBitmasks, dirty); + setConstantParameter(ma, "linearThickness", options.gtao.linearThickness, dirty); + if (dirty) { + ma->invalidate(); + // TODO: call Material::compile(), we can't do that now because it works only + // with surface materials + } + + ma = material.getMaterial(mEngine); FMaterialInstance* const mi = getMaterialInstance(ma); // Set AO type specific material parameters @@ -1028,6 +1038,7 @@ FrameGraphId PostProcessManager::screenSpaceAmbientOcclusion( mi->setParameter("radius", options.radius); mi->setParameter("intensity", options.intensity); mi->setParameter("thicknessHeuristic", options.gtao.thicknessHeuristic); + mi->setParameter("constThickness", options.gtao.constThickness); break; } diff --git a/filament/src/materials/ssao/gtao.mat b/filament/src/materials/ssao/gtao.mat index d7fd2b2a7f..ce949218e6 100644 --- a/filament/src/materials/ssao/gtao.mat +++ b/filament/src/materials/ssao/gtao.mat @@ -65,6 +65,10 @@ material { type : float, name : thicknessHeuristic }, + { + type : float, + name : constThickness + }, { type : float, name : ssctShadowDistance @@ -98,6 +102,18 @@ material { name : ssctSampleCount } ], + constants : [ + { + type : bool, + name : useVisibilityBitmasks, + default : false + }, + { + type : bool, + name : linearThickness, + default : false + } + ], variables : [ vertex ], diff --git a/filament/src/materials/ssao/gtaoBentNormals.mat b/filament/src/materials/ssao/gtaoBentNormals.mat index 91cd414775..cb6e7065b5 100644 --- a/filament/src/materials/ssao/gtaoBentNormals.mat +++ b/filament/src/materials/ssao/gtaoBentNormals.mat @@ -65,6 +65,10 @@ material { type : float, name : thicknessHeuristic }, + { + type : float, + name : constThickness + }, { type : float, name : ssctShadowDistance @@ -98,6 +102,18 @@ material { name : ssctSampleCount } ], + constants : [ + { + type : bool, + name : useVisibilityBitmasks, + default : false + }, + { + type : bool, + name : linearThickness, + default : false + } + ], outputs : [ { name : aoData, diff --git a/filament/src/materials/ssao/gtaoImpl.fs b/filament/src/materials/ssao/gtaoImpl.fs index 4816c50166..7f74ba6f81 100644 --- a/filament/src/materials/ssao/gtaoImpl.fs +++ b/filament/src/materials/ssao/gtaoImpl.fs @@ -24,6 +24,7 @@ #include "../utils/geometry.fs" #define rsqrt inversesqrt +#define SECTOR_COUNT 32u #ifndef COMPUTE_BENT_NORMAL #error COMPUTE_BENT_NORMAL must be set @@ -58,6 +59,67 @@ float spatialOffsetsNoise(float2 uv) { return 0.25 * float((position.y - position.x) & 3); } +// If the new sample value is greater then the current one, update the value with some fallOff. +// Otherwise, apply thicknessHeuristic. +float updateHorizon(float sampleHorizonCos, float currentHorizonCos, float fallOff) { + return sampleHorizonCos > currentHorizonCos + ? mix(sampleHorizonCos, currentHorizonCos, fallOff) + : mix(currentHorizonCos, sampleHorizonCos, materialParams.thicknessHeuristic); +} + +float calculateHorizonCos(highp vec3 sampleDelta, highp vec3 viewDir, float horizonCos) { + highp float sqSampleDist = dot(sampleDelta, sampleDelta); + float invSampleDist = rsqrt(sqSampleDist); + + // Use the view space radius to calculate the fallOff + float fallOff = saturate(sqSampleDist * materialParams.invRadiusSquared * 2.0); + + // sample horizon cos + float shc = dot(sampleDelta, viewDir) * invSampleDist; + + return updateHorizon(shc, horizonCos, fallOff); +} + +// https://cdrinmatane.github.io/posts/ssaovb-code/ +// https://github.com/cdrinmatane/SSRT3/blob/main/HDRP/Shaders/Resources/SSRTCS.compute +uint updateSectors(float minHorizon, float maxHorizon, uint globalOccludedBitfield) { + uint startHorizonInt = uint(minHorizon * float(SECTOR_COUNT)); + uint angleHorizonInt = uint(ceil(saturate(maxHorizon-minHorizon) * float(SECTOR_COUNT))); + uint angleHorizonBitfield = angleHorizonInt > 0u ? (0xFFFFFFFFu >> (SECTOR_COUNT-angleHorizonInt)) : 0u; + uint currentOccludedBitfield = angleHorizonBitfield << startHorizonInt; + return globalOccludedBitfield | currentOccludedBitfield; +} + +// https://cdrinmatane.github.io/posts/ssaovb-code/ +// https://github.com/cdrinmatane/SSRT3/blob/main/HDRP/Shaders/Resources/SSRTCS.compute +// The visibility bitmask method replaces the traditional two horizon angles with a bitmask +// for each slice. This bitmask flags whether each sector is occluded or not, +// which enables surfaces to be modeled with constant thickness, overcoming the limitation +// of treating them as a simple height field. +uint calculateVisibilityMask(highp vec3 deltaPos, highp vec3 viewDir, float samplingDirection, + uint globalOccludedBitfield, float n, highp vec3 origin) { + vec2 frontBackHorizon; + float linearThicknessMultiplier = materialConstants_linearThickness + ? saturate(origin.z * materialParams.invFarPlane) * 100.0 + : 1.0; + vec3 deltaPosBackface = deltaPos - viewDir * materialParams.constThickness * linearThicknessMultiplier; + + // Project sample onto the unit circle and compute the angle relative to V + frontBackHorizon.x = dot(normalize(deltaPos), viewDir); + frontBackHorizon.y = dot(normalize(deltaPosBackface), viewDir); + + frontBackHorizon.x = acosFast(frontBackHorizon.x); + frontBackHorizon.y = acosFast(frontBackHorizon.y); + + // Shift sample from V to normal, clamp in [0..1] + frontBackHorizon = clamp(((samplingDirection * -frontBackHorizon) + n + HALF_PI) / PI, 0.0, 1.0); + + // Sampling direction inverts min/max angles + frontBackHorizon = samplingDirection >= 0.0 ? frontBackHorizon.yx : frontBackHorizon.xy; + + return updateSectors(frontBackHorizon.x, frontBackHorizon.y, globalOccludedBitfield); +} + void groundTruthAmbientOcclusion(out float obscurance, out vec3 bentNormal, highp vec2 uv, highp vec3 origin, vec3 normal) { vec2 uvSamplePos = uv; @@ -84,20 +146,26 @@ void groundTruthAmbientOcclusion(out float obscurance, out vec3 bentNormal, vec3 direction = vec3(cosPhi, sinPhi, 0.0); // Project direction onto the plane orthogonal to viewDir. vec3 orthoDirection = normalize(direction - (dot(direction, viewDir)*viewDir)); - // axis is orthogonal to direction and viewDir (basically the normal of the slice plane) + // `axis` is orthogonal to direction and viewDir (basically the normal of the slice plane) // Used to define projectedNormal vec3 axis = cross(orthoDirection, viewDir); - // Project the normal onto the slice plane + // `projNormal` is the normal projected onto the slice plane vec3 projNormal = normal - axis * dot(normal, axis); float signNorm = sign(dot(orthoDirection, projNormal)); float projNormalLength = length(projNormal); float cosNorm = saturate(dot(projNormal, viewDir) / projNormalLength); + // `n` is the signed angle between projNormal and viewDir float n = signNorm * acosFast(cosNorm); + // These are only used in non-bitmask mode float horizonCos0 = -1.0; float horizonCos1 = -1.0; + + // This is only used in bitmask mode + uint globalOccludedBitfield = 0u; + for (float j = 0.0; j < materialParams.stepsPerSlice; j += 1.0) { // At least move 1 pixel forward in the screen-space vec2 sampleOffset = max((j + initialRayStep)*stepRadius, 1.0 + j) * omega; @@ -116,33 +184,36 @@ void groundTruthAmbientOcclusion(out float obscurance, out vec3 bentNormal, highp vec3 sampleDelta0 = (samplePos0 - origin); highp vec3 sampleDelta1 = (samplePos1 - origin); - highp vec2 sqSampleDist = vec2(dot(sampleDelta0, sampleDelta0), dot(sampleDelta1, sampleDelta1)); - vec2 invSampleDist = rsqrt(sqSampleDist); - // Use the view space radius to calculate the fallOff - vec2 fallOff = saturate(sqSampleDist.xy * materialParams.invRadiusSquared * 2.0); - - // sample horizon cos - float shc0 = dot(sampleDelta0, viewDir) * invSampleDist.x; - float shc1 = dot(sampleDelta1, viewDir) * invSampleDist.y; - - // If the new sample value is greater then the current one, update the value with some fallOff. - // Otherwise, apply thicknessHeuristic. - horizonCos0 = shc0 > horizonCos0 ? mix(shc0, horizonCos0, fallOff.x) : mix(horizonCos0, shc0, materialParams.thicknessHeuristic); - horizonCos1 = shc1 > horizonCos1 ? mix(shc1, horizonCos1, fallOff.y) : mix(horizonCos1, shc1, materialParams.thicknessHeuristic); + if (materialConstants_useVisibilityBitmasks) { + globalOccludedBitfield = calculateVisibilityMask(sampleDelta0, viewDir, 1.0, globalOccludedBitfield, n, origin); + globalOccludedBitfield = calculateVisibilityMask(sampleDelta1, viewDir, -1.0, globalOccludedBitfield, n, origin); + } + else { + horizonCos0 = calculateHorizonCos(sampleDelta0, viewDir, horizonCos0); + horizonCos1 = calculateHorizonCos(sampleDelta1, viewDir, horizonCos1); + } } - float h0 = -acosFast(horizonCos1); - float h1 = acosFast(horizonCos0); - h0 = n + clamp(h0 - n, -HALF_PI, HALF_PI); - h1 = n + clamp(h1 - n, -HALF_PI, HALF_PI); + if (materialConstants_useVisibilityBitmasks) { + // Calculate the portion of the occluded sector + visibility += 1.0 - float(bitCount(globalOccludedBitfield)) / float(SECTOR_COUNT); -#if COMPUTE_BENT_NORMAL - float angle = 0.5 * (h0 + h1); - bentNormal += viewDir * cos(angle) - orthoDirection * sin(angle); -#endif + // Note: bent normal calculation is not supported in visibility bitmasks mode + } + else { + float h0 = -acosFast(horizonCos1); + float h1 = acosFast(horizonCos0); + h0 = n + clamp(h0 - n, -HALF_PI, HALF_PI); + h1 = n + clamp(h1 - n, -HALF_PI, HALF_PI); - visibility += projNormalLength * (integrateArcCosWeight(h0, n) + integrateArcCosWeight(h1, n)); + #if COMPUTE_BENT_NORMAL + float angle = 0.5 * (h0 + h1); + bentNormal += viewDir * cos(angle) - orthoDirection * sin(angle); + #endif + + visibility += projNormalLength * (integrateArcCosWeight(h0, n) + integrateArcCosWeight(h1, n)); + } } obscurance = 1.0 - saturate(visibility * materialParams.sliceCount.y); diff --git a/libs/viewer/src/Settings_generated.cpp b/libs/viewer/src/Settings_generated.cpp index 1b2a4fa406..0dc7944e6b 100644 --- a/libs/viewer/src/Settings_generated.cpp +++ b/libs/viewer/src/Settings_generated.cpp @@ -586,6 +586,12 @@ int parse(jsmntok_t const* tokens, int i, const char* jsonChunk, AmbientOcclusio i = parse(tokens, i + 1, jsonChunk, &out->sampleStepsPerSlice); } else if (compare(tok, jsonChunk, "thicknessHeuristic") == 0) { i = parse(tokens, i + 1, jsonChunk, &out->thicknessHeuristic); + } else if (compare(tok, jsonChunk, "useVisibilityBitmasks") == 0) { + i = parse(tokens, i + 1, jsonChunk, &out->useVisibilityBitmasks); + } else if (compare(tok, jsonChunk, "constThickness") == 0) { + i = parse(tokens, i + 1, jsonChunk, &out->constThickness); + } else if (compare(tok, jsonChunk, "linearThickness") == 0) { + i = parse(tokens, i + 1, jsonChunk, &out->linearThickness); } else { slog.w << "Invalid Gtao key: '" << STR(tok, jsonChunk) << "'" << io::endl; i = parse(tokens, i + 1); @@ -602,7 +608,10 @@ std::ostream& operator<<(std::ostream& out, const AmbientOcclusionOptions::Gtao& return out << "{\n" << "\"sampleSliceCount\": " << int(in.sampleSliceCount) << ",\n" << "\"sampleStepsPerSlice\": " << int(in.sampleStepsPerSlice) << ",\n" - << "\"thicknessHeuristic\": " << (in.thicknessHeuristic) << "\n" + << "\"thicknessHeuristic\": " << (in.thicknessHeuristic) << ",\n" + << "\"useVisibilityBitmasks\": " << to_string(in.useVisibilityBitmasks) << ",\n" + << "\"constThickness\": " << (in.constThickness) << ",\n" + << "\"linearThickness\": " << to_string(in.linearThickness) << "\n" << "}"; } diff --git a/libs/viewer/src/ViewerGui.cpp b/libs/viewer/src/ViewerGui.cpp index 1b2872f32c..ee7585a07f 100644 --- a/libs/viewer/src/ViewerGui.cpp +++ b/libs/viewer/src/ViewerGui.cpp @@ -854,6 +854,11 @@ void ViewerGui::updateUserInterface() { ImGui::SliderInt("Slice Count", &sliceCount, 1, 10); ImGui::SliderInt("Steps Per Slice", &stepsPerSlice, 1, 4); + ImGui::Checkbox("Use Visibility Bitmasks", &ssao.gtao.useVisibilityBitmasks); + if (ssao.gtao.useVisibilityBitmasks) { + ImGui::SliderFloat("Constant Thickness", &ssao.gtao.constThickness, 0.01f, 10.0f); + ImGui::Checkbox("Linear Thickness", &ssao.gtao.linearThickness); + } ssao.gtao.sampleSliceCount = static_cast(sliceCount); ssao.gtao.sampleStepsPerSlice = static_cast(stepsPerSlice); diff --git a/web/filament-js/extensions_generated.js b/web/filament-js/extensions_generated.js index c6fc55f7f0..b92b549b25 100644 --- a/web/filament-js/extensions_generated.js +++ b/web/filament-js/extensions_generated.js @@ -113,6 +113,9 @@ Filament.loadGeneratedExtensions = function() { sampleSliceCount: 4, sampleStepsPerSlice: 3, thicknessHeuristic: 0.004, + useVisibilityBitmasks: false, + constThickness: 0.5, + linearThickness: false, }; return Object.assign(options, overrides); }; diff --git a/web/filament-js/filament.d.ts b/web/filament-js/filament.d.ts index b2566e2b63..adf0863952 100644 --- a/web/filament-js/filament.d.ts +++ b/web/filament-js/filament.d.ts @@ -1595,9 +1595,23 @@ export interface View$AmbientOcclusionOptions$Gtao { */ sampleStepsPerSlice?: number; /** - * thickness heuristic, should be closed to 0 + * thickness heuristic, should be closed to 0. No effect when useVisibilityBitmasks sets to true. */ thicknessHeuristic?: number; + /** + * Enables or disables visibility bitmasks mode. Notes that bent normal doesn't work under this mode. + * Caution: Changing this option at runtime is very expensive as it may trigger a shader re-compilation. + */ + useVisibilityBitmasks?: boolean; + /** + * constant thickness value of objects on the screen in world space. Only take effect when useVisibilityBitmasks is set to true. + */ + constThickness?: number; + /** + * Increase thickness with distance to maintain detail on distant surfaces. + * Caution: Changing this option at runtime is very expensive as it may trigger a shader re-compilation. + */ + linearThickness?: boolean; } /** diff --git a/web/filament-js/jsbindings_generated.cpp b/web/filament-js/jsbindings_generated.cpp index ca7760e308..70c9ffdde2 100644 --- a/web/filament-js/jsbindings_generated.cpp +++ b/web/filament-js/jsbindings_generated.cpp @@ -99,6 +99,9 @@ value_object("View$AmbientOcclusionOptions$ .field("sampleSliceCount", &View::AmbientOcclusionOptions::Gtao::sampleSliceCount) .field("sampleStepsPerSlice", &View::AmbientOcclusionOptions::Gtao::sampleStepsPerSlice) .field("thicknessHeuristic", &View::AmbientOcclusionOptions::Gtao::thicknessHeuristic) + .field("useVisibilityBitmasks", &View::AmbientOcclusionOptions::Gtao::useVisibilityBitmasks) + .field("constThickness", &View::AmbientOcclusionOptions::Gtao::constThickness) + .field("linearThickness", &View::AmbientOcclusionOptions::Gtao::linearThickness) ; value_object("View$AmbientOcclusionOptions")