feat: Add Visibility Bitmask method for GTAO (#9101)

* Quick test

* Fix incorrect logic and add some comments

* Set thickness parameter

* refactoring

* Update

* Generated files

* Some comments

* Update

* Update generated files

* Update

* Update

* Add comments
This commit is contained in:
Doris Wu
2025-08-26 16:27:02 -07:00
committed by GitHub
parent 161b75b9b7
commit ff394f7c1b
11 changed files with 201 additions and 28 deletions

View File

@@ -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;
}

View File

@@ -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%
};

View File

@@ -993,7 +993,17 @@ FrameGraphId<FrameGraphTexture> 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<FrameGraphTexture> 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;
}

View File

@@ -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
],

View File

@@ -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,

View File

@@ -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);

View File

@@ -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"
<< "}";
}

View File

@@ -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<uint8_t>(sliceCount);
ssao.gtao.sampleStepsPerSlice = static_cast<uint8_t>(stepsPerSlice);

View File

@@ -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);
};

View File

@@ -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;
}
/**

View File

@@ -99,6 +99,9 @@ value_object<View::AmbientOcclusionOptions::Gtao>("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>("View$AmbientOcclusionOptions")